From 0c15b91c834926ef4820fdb66afa17f1336b8d80 Mon Sep 17 00:00:00 2001 From: cyb3rdog Date: Sun, 31 Jan 2021 22:24:28 +0100 Subject: [PATCH 01/15] version 0.7.2 - merged commit ( 5.2 & 6.2 & 7.1 ) --- anki_vector/animation.py | 2 + anki_vector/annotate.py | 4 +- anki_vector/audio.py | 4 + anki_vector/behavior.py | 177 ++- anki_vector/camera.py | 240 ++- anki_vector/configure/__main__.py | 6 +- anki_vector/connection.py | 38 +- anki_vector/events.py | 16 +- anki_vector/exceptions.py | 5 + anki_vector/faces.py | 98 +- anki_vector/messaging/alexa.proto | 37 + .../messaging/external_interface.proto | 133 +- .../messaging/external_interface_pb2.py | 143 +- .../messaging/external_interface_pb2_grpc.py | 85 +- anki_vector/messaging/messages.proto | 275 +++- anki_vector/messaging/messages_pb2.py | 1333 +++++++++++++---- anki_vector/messaging/settings.proto | 155 ++ anki_vector/messaging/shared.proto | 13 +- anki_vector/messaging/shared_pb2.py | 83 +- anki_vector/objects.py | 21 +- anki_vector/robot.py | 11 + anki_vector/version.py | 2 +- anki_vector/vision.py | 72 +- anki_vector/world.py | 1 + 24 files changed, 2458 insertions(+), 496 deletions(-) diff --git a/anki_vector/animation.py b/anki_vector/animation.py index ddbdd3e..58490a3 100644 --- a/anki_vector/animation.py +++ b/anki_vector/animation.py @@ -190,6 +190,7 @@ async def load_animation_trigger_list(self): """ return await self._load_animation_trigger_list() + # TODO: add return type hint @connection.on_connection_thread() async def play_animation_trigger(self, anim_trigger: str, loop_count: int = 1, use_lift_safe: bool = False, ignore_body_track: bool = False, ignore_head_track: bool = False, ignore_lift_track: bool = False): # START """Starts an animation trigger playing on a robot. @@ -227,6 +228,7 @@ async def play_animation_trigger(self, anim_trigger: str, loop_count: int = 1, u ignore_lift_track=ignore_lift_track) return await self.grpc_interface.PlayAnimationTrigger(req) + # TODO: add return type hint @connection.on_connection_thread() async def play_animation(self, anim: str, loop_count: int = 1, ignore_body_track: bool = False, ignore_head_track: bool = False, ignore_lift_track: bool = False): """Starts an animation playing on a robot. diff --git a/anki_vector/annotate.py b/anki_vector/annotate.py index 5a07bd8..73eb98c 100644 --- a/anki_vector/annotate.py +++ b/anki_vector/annotate.py @@ -43,7 +43,7 @@ from typing import Callable, Iterable, Tuple, Union try: - from PIL import Image, ImageDraw, ImageFont + from PIL import Image, ImageDraw except ImportError: sys.exit("Cannot import from PIL: Do `pip3 install --user Pillow` to install") except SyntaxError: @@ -137,7 +137,7 @@ def clock(image, scale, annotator=None, world=None, **kw): """ def __init__(self, text: str, position: int = AnnotationPosition.BOTTOM_RIGHT, align: str = "left", color: str = "white", - font = None, line_spacing: int = 3, outline_color: str = None, full_outline: bool = True): + font=None, line_spacing: int = 3, outline_color: str = None, full_outline: bool = True): self.text = text self.position = position self.align = align diff --git a/anki_vector/audio.py b/anki_vector/audio.py index 58ef29c..19a8a32 100644 --- a/anki_vector/audio.py +++ b/anki_vector/audio.py @@ -15,6 +15,7 @@ """Support for accessing Vector's audio. Vector's speakers can be used for playing user-provided audio. +TODO Ability to access the Vector's audio stream to come. The :class:`AudioComponent` class defined in this module is made available as :attr:`anki_vector.robot.Robot.audio` and can be used to play audio data on the robot. @@ -55,6 +56,7 @@ class AudioComponent(util.Component): """Handles audio on Vector. The AudioComponent object plays audio data to Vector's speaker. + Ability to access the Vector's audio stream to come. The :class:`anki_vector.robot.Robot` or :class:`anki_vector.robot.AsyncRobot` instance owns this audio component. @@ -67,6 +69,8 @@ class AudioComponent(util.Component): robot.audio.stream_wav_file('../examples/sounds/vector_alert.wav') """ + # TODO restore audio feed code when ready + def __init__(self, robot): super().__init__(robot) self._is_shutdown = False diff --git a/anki_vector/behavior.py b/anki_vector/behavior.py index d4555fc..7e37d0f 100644 --- a/anki_vector/behavior.py +++ b/anki_vector/behavior.py @@ -67,26 +67,29 @@ class BehaviorComponent(util.Component): """Run behaviors on Vector""" - _next_behavior_id = protocol.FIRST_SDK_TAG + _next_action_id = protocol.FIRST_SDK_TAG @classmethod - def _get_next_behavior_id(cls): - # Post increment _current_behavior_id (and loop within the SDK_TAG range) - next_behavior_id = cls._next_behavior_id - if cls._next_behavior_id == protocol.LAST_SDK_TAG: - cls._next_behavior_id = protocol.FIRST_SDK_TAG + def _get_next_action_id(cls): + # Post increment _current_action_id (and loop within the SDK_TAG range) + next_action_id = cls._next_action_id + if cls._next_action_id == protocol.LAST_SDK_TAG: + cls._next_action_id = protocol.FIRST_SDK_TAG else: - cls._next_behavior_id += 1 - return next_behavior_id + cls._next_action_id += 1 + return next_action_id @connection.on_connection_thread() - async def _abort(self, behavior_id): - # TODO Currently cancels actions only. Add ability to cancel behaviors. - cancel_action_request = protocol.CancelActionByIdTagRequest(id_tag=behavior_id) + async def _abort_action(self, action_id): + cancel_action_request = protocol.CancelActionByIdTagRequest(id_tag=action_id) return await self.grpc_interface.CancelActionByIdTag(cancel_action_request) - # TODO Make this cancellable with is_cancellable_behavior @connection.on_connection_thread() + async def _abort_behavior(self): + cancel_behavior_request = protocol.CancelBehaviorRequest() + return await self.grpc_interface.CancelBehavior(cancel_behavior_request) + + @connection.on_connection_thread(is_cancellable=connection.CancelType.CANCELLABLE_BEHAVIOR) async def drive_off_charger(self) -> protocol.DriveOffChargerResponse: """Drive Vector off the charger @@ -98,12 +101,23 @@ async def drive_off_charger(self) -> protocol.DriveOffChargerResponse: with anki_vector.Robot() as robot: robot.behavior.drive_off_charger() + + Example of cancelling the :meth:`drive_off_charger` behavior: + + .. testcode:: + + import anki_vector + import time + + with anki_vector.AsyncRobot() as robot: + drive_off_future = robot.behavior.drive_off_charger() + time.sleep(3.0) + drive_off_future.cancel() """ drive_off_charger_request = protocol.DriveOffChargerRequest() return await self.grpc_interface.DriveOffCharger(drive_off_charger_request) - # TODO Make this cancellable with is_cancellable_behavior - @connection.on_connection_thread() + @connection.on_connection_thread(is_cancellable=connection.CancelType.CANCELLABLE_BEHAVIOR) async def drive_on_charger(self) -> protocol.DriveOnChargerResponse: """Drive Vector onto the charger @@ -119,12 +133,23 @@ async def drive_on_charger(self) -> protocol.DriveOnChargerResponse: with anki_vector.Robot() as robot: robot.behavior.drive_on_charger() + + Example of cancelling the :meth:`drive_on_charger` behavior: + + .. testcode:: + + import anki_vector + import time + + with anki_vector.AsyncRobot() as robot: + drive_on_future = robot.behavior.drive_on_charger() + time.sleep(3.0) + drive_on_future.cancel() """ drive_on_charger_request = protocol.DriveOnChargerRequest() return await self.grpc_interface.DriveOnCharger(drive_on_charger_request) - # TODO Make this cancellable with is_cancellable_behavior - @connection.on_connection_thread() + @connection.on_connection_thread(is_cancellable=connection.CancelType.CANCELLABLE_BEHAVIOR) async def find_faces(self) -> protocol.FindFacesResponse: """Look around for faces @@ -136,12 +161,23 @@ async def find_faces(self) -> protocol.FindFacesResponse: with anki_vector.Robot() as robot: robot.behavior.find_faces() + + Example of cancelling the :meth:`find_faces` behavior: + + .. testcode:: + + import anki_vector + import time + + with anki_vector.AsyncRobot() as robot: + find_faces_future = robot.behavior.find_faces() + time.sleep(3.0) + find_faces_future.cancel() """ find_faces_request = protocol.FindFacesRequest() return await self.grpc_interface.FindFaces(find_faces_request) - # TODO Make this cancellable with is_cancellable_behavior - @connection.on_connection_thread() + @connection.on_connection_thread(is_cancellable=connection.CancelType.CANCELLABLE_BEHAVIOR) async def look_around_in_place(self) -> protocol.LookAroundInPlaceResponse: """Look around in place @@ -153,12 +189,23 @@ async def look_around_in_place(self) -> protocol.LookAroundInPlaceResponse: with anki_vector.Robot() as robot: robot.behavior.look_around_in_place() + + Example of cancelling the :meth:`look_around_in_place` behavior: + + .. testcode:: + + import anki_vector + import time + + with anki_vector.AsyncRobot() as robot: + look_around_in_place_future = robot.behavior.look_around_in_place() + time.sleep(3.0) + look_around_in_place_future.cancel() """ look_around_in_place_request = protocol.LookAroundInPlaceRequest() return await self.grpc_interface.LookAroundInPlace(look_around_in_place_request) - # TODO Make this cancellable with is_cancellable_behavior - @connection.on_connection_thread() + @connection.on_connection_thread(is_cancellable=connection.CancelType.CANCELLABLE_BEHAVIOR) async def roll_visible_cube(self) -> protocol.RollBlockResponse: """Roll a cube that is currently known to the robot @@ -173,11 +220,23 @@ async def roll_visible_cube(self) -> protocol.RollBlockResponse: with anki_vector.Robot() as robot: robot.behavior.roll_visible_cube() + + Example of cancelling the :meth:`roll_visible_cube` behavior: + + .. testcode:: + + import anki_vector + import time + + with anki_vector.AsyncRobot() as robot: + roll_visible_cube_future = robot.behavior.roll_visible_cube() + time.sleep(3.0) + roll_visible_cube_future.cancel() """ roll_block_request = protocol.RollBlockRequest() return await self.grpc_interface.RollBlock(roll_block_request) - # TODO Make this cancellable with is_cancellable_behavior + # TODO Make this cancellable with is_cancellable @connection.on_connection_thread() async def say_text(self, text: str, use_vector_voice: bool = True, duration_scalar: float = 1.0) -> protocol.SayTextResponse: """Make Vector speak text. @@ -201,7 +260,7 @@ async def say_text(self, text: str, use_vector_voice: bool = True, duration_scal duration_scalar=duration_scalar) return await self.conn.grpc_interface.SayText(say_text_request) - # TODO Make this cancellable with is_cancellable_behavior? + # TODO Make this cancellable with is_cancellable? @connection.on_connection_thread() async def set_eye_color(self, hue: float, saturation: float) -> protocol.SetEyeColorResponse: """Set Vector's eye color. @@ -230,12 +289,12 @@ async def set_eye_color(self, hue: float, saturation: float) -> protocol.SetEyeC eye_color_request = protocol.SetEyeColorRequest(hue=hue, saturation=saturation) return await self.conn.grpc_interface.SetEyeColor(eye_color_request) - @connection.on_connection_thread(is_cancellable_behavior=True) + @connection.on_connection_thread(is_cancellable=connection.CancelType.CANCELLABLE_ACTION) async def go_to_pose(self, pose: util.Pose, relative_to_robot: bool = False, num_retries: int = 0, - _behavior_id: int = None) -> protocol.GoToPoseResponse: + _action_id: int = None) -> protocol.GoToPoseResponse: """Tells Vector to drive to the specified pose and orientation. In navigating to the requested pose, Vector will use path planning. @@ -289,20 +348,20 @@ async def go_to_pose(self, go_to_pose_request = protocol.GoToPoseRequest(x_mm=pose.position.x, y_mm=pose.position.y, rad=pose.rotation.angle_z.radians, - id_tag=_behavior_id, + id_tag=_action_id, num_retries=num_retries) return await self.grpc_interface.GoToPose(go_to_pose_request) # TODO alignment_type coming out ugly in the docs without real values - @connection.on_connection_thread(is_cancellable_behavior=True) + @connection.on_connection_thread(is_cancellable=connection.CancelType.CANCELLABLE_ACTION) async def dock_with_cube(self, target_object: objects.LightCube, approach_angle: util.Angle = None, alignment_type: protocol.AlignmentType = protocol.ALIGNMENT_TYPE_LIFT_PLATE, distance_from_marker: util.Distance = None, num_retries: int = 0, - _behavior_id: int = None) -> protocol.DockWithCubeResponse: + _action_id: int = None) -> protocol.DockWithCubeResponse: """Tells Vector to dock with a light cube, optionally using a given approach angle and distance. While docking with the cube, Vector will use path planning. @@ -360,7 +419,7 @@ async def dock_with_cube(self, dock_request = protocol.DockWithCubeRequest(object_id=target_object.object_id, alignment_type=alignment_type, - id_tag=_behavior_id, + id_tag=_action_id, num_retries=num_retries) if approach_angle is not None: dock_request.use_approach_angle = True @@ -372,13 +431,13 @@ async def dock_with_cube(self, return await self.grpc_interface.DockWithCube(dock_request) # Movement actions - @connection.on_connection_thread(is_cancellable_behavior=True) + @connection.on_connection_thread(is_cancellable=connection.CancelType.CANCELLABLE_ACTION) async def drive_straight(self, distance: util.Distance, speed: util.Speed, should_play_anim: bool = True, num_retries: int = 0, - _behavior_id: int = None) -> protocol.DriveStraightResponse: + _action_id: int = None) -> protocol.DriveStraightResponse: """Tells Vector to drive in a straight line. Vector will drive for the specified distance (forwards or backwards) @@ -424,12 +483,12 @@ async def drive_straight(self, drive_straight_request = protocol.DriveStraightRequest(speed_mmps=speed.speed_mmps, dist_mm=distance.distance_mm, should_play_animation=should_play_anim, - id_tag=_behavior_id, + id_tag=_action_id, num_retries=num_retries) return await self.grpc_interface.DriveStraight(drive_straight_request) - @connection.on_connection_thread(is_cancellable_behavior=True) + @connection.on_connection_thread(is_cancellable=connection.CancelType.CANCELLABLE_ACTION) async def turn_in_place(self, angle: util.Angle, speed: util.Angle = util.Angle(0.0), @@ -437,7 +496,7 @@ async def turn_in_place(self, angle_tolerance: util.Angle = util.Angle(0.0), is_absolute: bool = 0, num_retries: int = 0, - _behavior_id: int = None) -> protocol.TurnInPlaceResponse: + _action_id: int = None) -> protocol.TurnInPlaceResponse: """Turn the robot around its current position. Vector must be off of the charger for this movement action. @@ -487,19 +546,19 @@ async def turn_in_place(self, accel_rad_per_sec2=accel.radians, tol_rad=angle_tolerance.radians, is_absolute=is_absolute, - id_tag=_behavior_id, + id_tag=_action_id, num_retries=num_retries) return await self.grpc_interface.TurnInPlace(turn_in_place_request) - @connection.on_connection_thread(is_cancellable_behavior=True) + @connection.on_connection_thread(is_cancellable=connection.CancelType.CANCELLABLE_ACTION) async def set_head_angle(self, angle: util.Angle, accel: float = 10.0, max_speed: float = 10.0, duration: float = 0.0, num_retries: int = 0, - _behavior_id: int = None) -> protocol.SetHeadAngleResponse: + _action_id: int = None) -> protocol.SetHeadAngleResponse: """Tell Vector's head to move to a given angle. :param angle: Desired angle for Vector's head. @@ -558,18 +617,18 @@ async def set_head_angle(self, max_speed_rad_per_sec=max_speed, accel_rad_per_sec2=accel, duration_sec=duration, - id_tag=_behavior_id, + id_tag=_action_id, num_retries=num_retries) return await self.grpc_interface.SetHeadAngle(set_head_angle_request) - @connection.on_connection_thread(is_cancellable_behavior=True) + @connection.on_connection_thread(is_cancellable=connection.CancelType.CANCELLABLE_ACTION) async def set_lift_height(self, height: float, accel: float = 10.0, max_speed: float = 10.0, duration: float = 0.0, num_retries: int = 0, - _behavior_id: int = None) -> protocol.SetLiftHeightResponse: + _action_id: int = None) -> protocol.SetLiftHeightResponse: """Tell Vector's lift to move to a given height. :param height: desired height for Vector's lift 0.0 (bottom) to @@ -621,16 +680,16 @@ async def set_lift_height(self, max_speed_rad_per_sec=max_speed, accel_rad_per_sec2=accel, duration_sec=duration, - id_tag=_behavior_id, + id_tag=_action_id, num_retries=num_retries) return await self.grpc_interface.SetLiftHeight(set_lift_height_request) - @connection.on_connection_thread(is_cancellable_behavior=True) + @connection.on_connection_thread(is_cancellable=connection.CancelType.CANCELLABLE_ACTION) async def turn_towards_face(self, face: faces.Face, num_retries: int = 0, - _behavior_id: int = None) -> protocol.TurnTowardsFaceResponse: + _action_id: int = None) -> protocol.TurnTowardsFaceResponse: """Tells Vector to turn towards this face. :param face_id: The face Vector will turn towards. @@ -658,17 +717,17 @@ async def turn_towards_face(self, """ turn_towards_face_request = protocol.TurnTowardsFaceRequest(face_id=face.face_id, max_turn_angle_rad=util.degrees(180).radians, - id_tag=_behavior_id, + id_tag=_action_id, num_retries=num_retries) return await self.grpc_interface.TurnTowardsFace(turn_towards_face_request) - @connection.on_connection_thread(is_cancellable_behavior=True) + @connection.on_connection_thread(is_cancellable=connection.CancelType.CANCELLABLE_ACTION) async def go_to_object(self, target_object: objects.LightCube, distance_from_object, num_retries: int = 0, - _behavior_id: int = None) -> protocol.GoToObjectResponse: + _action_id: int = None) -> protocol.GoToObjectResponse: """Tells Vector to drive to his Cube. :param target_object: The destination object. CustomObject instances are not supported. @@ -696,17 +755,17 @@ async def go_to_object(self, go_to_object_request = protocol.GoToObjectRequest(object_id=target_object.object_id, distance_from_object_origin_mm=distance_from_object.distance_mm, use_pre_dock_pose=False, - id_tag=_behavior_id, + id_tag=_action_id, num_retries=num_retries) return await self.grpc_interface.GoToObject(go_to_object_request) - @connection.on_connection_thread(is_cancellable_behavior=True) + @connection.on_connection_thread(is_cancellable=connection.CancelType.CANCELLABLE_ACTION) async def roll_cube(self, target_object: objects.LightCube, approach_angle: util.Angle = None, num_retries: int = 0, - _behavior_id: int = None) -> protocol.RollObjectResponse: + _action_id: int = None) -> protocol.RollObjectResponse: """Tells Vector to roll a specified cube object. :param target_object: The cube to roll. @@ -742,17 +801,17 @@ async def roll_cube(self, approach_angle_rad=approach_angle.radians, use_approach_angle=use_approach_angle, use_pre_dock_pose=use_approach_angle, - id_tag=_behavior_id, + id_tag=_action_id, num_retries=num_retries) return await self.grpc_interface.RollObject(roll_object_request) - @connection.on_connection_thread(is_cancellable_behavior=True) + @connection.on_connection_thread(is_cancellable=connection.CancelType.CANCELLABLE_ACTION) async def pop_a_wheelie(self, target_object: objects.LightCube, approach_angle: util.Angle = None, num_retries: int = 0, - _behavior_id: int = None) -> protocol.PopAWheelieResponse: + _action_id: int = None) -> protocol.PopAWheelieResponse: """Tells Vector to "pop a wheelie" using his light cube. :param target_object: The cube to push down on with Vector's lift, to start the wheelie. @@ -788,17 +847,17 @@ async def pop_a_wheelie(self, approach_angle_rad=approach_angle.radians, use_approach_angle=use_approach_angle, use_pre_dock_pose=use_approach_angle, - id_tag=_behavior_id, + id_tag=_action_id, num_retries=num_retries) return await self.grpc_interface.PopAWheelie(pop_a_wheelie_request) - @connection.on_connection_thread(is_cancellable_behavior=True) + @connection.on_connection_thread(is_cancellable=connection.CancelType.CANCELLABLE_ACTION) async def pickup_object(self, target_object: objects.LightCube, use_pre_dock_pose: bool = True, num_retries: int = 0, - _behavior_id: int = None) -> protocol.PickupObjectResponse: + _action_id: int = None) -> protocol.PickupObjectResponse: """Instruct the robot to pick up his LightCube. While picking up the cube, Vector will use path planning. @@ -830,15 +889,15 @@ async def pickup_object(self, pickup_object_request = protocol.PickupObjectRequest(object_id=target_object.object_id, use_pre_dock_pose=use_pre_dock_pose, - id_tag=_behavior_id, + id_tag=_action_id, num_retries=num_retries) return await self.grpc_interface.PickupObject(pickup_object_request) - @connection.on_connection_thread(is_cancellable_behavior=True) + @connection.on_connection_thread(is_cancellable=connection.CancelType.CANCELLABLE_ACTION) async def place_object_on_ground_here(self, num_retries: int = 0, - _behavior_id: int = None) -> protocol.PlaceObjectOnGroundHereResponse: + _action_id: int = None) -> protocol.PlaceObjectOnGroundHereResponse: """Ask Vector to place the object he is carrying on the ground at the current location. :param num_retries: Number of times to reattempt action in case of a failure. @@ -857,7 +916,7 @@ async def place_object_on_ground_here(self, robot.behavior.pickup_object(robot.world.connected_light_cube) robot.behavior.place_object_on_ground_here() """ - place_object_on_ground_here_request = protocol.PlaceObjectOnGroundHereRequest(id_tag=_behavior_id, + place_object_on_ground_here_request = protocol.PlaceObjectOnGroundHereRequest(id_tag=_action_id, num_retries=num_retries) return await self.grpc_interface.PlaceObjectOnGroundHere(place_object_on_ground_here_request) @@ -939,7 +998,7 @@ def __init__(self, '{"name":"Vector-XXXX", "ip":"XX.XX.XX.XX", "cert":"/path/to/cert_file", "guid":""}') self._conn = connection.Connection(self._name, ':'.join([self._ip, self._port]), self._cert_file, self._guid, - behavior_control_level=connection.CONTROL_PRIORITY_LEVEL.RESERVE_CONTROL) + behavior_control_level=connection.ControlPriorityLevel.RESERVE_CONTROL) self._behavior_activation_timeout = behavior_activation_timeout def __enter__(self): diff --git a/anki_vector/camera.py b/anki_vector/camera.py index 379c08d..af113d8 100644 --- a/anki_vector/camera.py +++ b/anki_vector/camera.py @@ -26,7 +26,7 @@ # __all__ should order by constants, event classes, other classes, functions. __all__ = ["EvtNewRawCameraImage", "EvtNewCameraImage", - "CameraComponent", "CameraImage"] + "CameraComponent", "CameraConfig", "CameraImage"] import asyncio from concurrent.futures import CancelledError @@ -36,7 +36,7 @@ from . import annotate, connection, util from .events import Events -from .exceptions import VectorCameraFeedException +from .exceptions import VectorCameraFeedException, VectorCameraImageCaptureException from .messaging import protocol try: @@ -129,12 +129,124 @@ def annotate_image(self, scale: float = None, fit_size: tuple = None, resample_m (fast) or :attr:`~anki_vector.annotate.RESAMPLE_MODE_BILINEAR` (slower, but smoother). """ + if self._raw_image.size != (640, 360): + raise VectorCameraImageCaptureException("Annotation is only supported for default resolution images.") return self._image_annotator.annotate_image(self._raw_image, scale=scale, fit_size=fit_size, resample_mode=resample_mode) +class CameraConfig: + """ The fixed properties for Vector's camera. + + A full 3x3 calibration matrix for doing 3D reasoning based on the camera + images would look like: + + +--------------+--------------+---------------+ + |focal_length.x| 0 | center.x | + +--------------+--------------+---------------+ + | 0 |focal_length.y| center.y | + +--------------+--------------+---------------+ + | 0 | 0 | 1 | + +--------------+--------------+---------------+ + + .. testcode:: + + import anki_vector + + with anki_vector.Robot() as robot: + min = robot.camera.config.min_gain + max = robot.camera.config.max_gain + print(f"Robot camera allowable exposure gain range is from {min} to {max}") + """ + + def __init__(self, + focal_length_x: float, + focal_length_y: float, + center_x: float, + center_y: float, + fov_x: float, + fov_y: float, + min_exposure_time_ms: int, + max_exposure_time_ms: int, + min_gain: float, + max_gain: float): + self._focal_length = util.Vector2(focal_length_x, focal_length_y) + self._center = util.Vector2(center_x, center_y) + self._fov_x = util.degrees(fov_x) + self._fov_y = util.degrees(fov_y) + self._min_exposure_ms = min_exposure_time_ms + self._max_exposure_ms = max_exposure_time_ms + self._min_gain = min_gain + self._max_gain = max_gain + + @classmethod + def create_from_message(cls, msg: protocol.CameraConfigResponse): + """Create camera configuration based on Vector's camera configuration from the message sent from the Robot """ + return cls(msg.focal_length_x, + msg.focal_length_y, + msg.center_x, + msg.center_y, + msg.fov_x, + msg.fov_y, + msg.min_camera_exposure_time_ms, + msg.max_camera_exposure_time_ms, + msg.min_camera_gain, + msg.max_camera_gain) + + @property + def min_gain(self) -> float: + """The minimum supported camera gain.""" + return self._min_gain + + @property + def max_gain(self) -> float: + """The maximum supported camera gain.""" + return self._max_gain + + @property + def min_exposure_time_ms(self) -> int: + """The minimum supported exposure time in milliseconds.""" + return self._min_exposure_ms + + @property + def max_exposure_time_ms(self) -> int: + """The maximum supported exposure time in milliseconds.""" + return self._max_exposure_ms + + @property + def focal_length(self): + """:class:`anki_vector.util.Vector2`: The focal length of the camera. + + This is focal length combined with pixel skew (as the pixels aren't + perfectly square), so there are subtly different values for x and y. + It is in floating point pixel values e.g. <288.87, 288.36>. + """ + return self._focal_length + + @property + def center(self): + """:class:`anki_vector.util.Vector2`: The focal center of the camera. + + This is the position of the optical center of projection within the + image. It will be close to the center of the image, but adjusted based + on the calibration of the lens. It is in floating point pixel values + e.g. <155.11, 111.40>. + """ + return self._center + + @property + def fov_x(self): + """:class:`anki_vector.util.Angle`: The x (horizontal) field of view.""" + return self._fov_x + + @property + def fov_y(self): + """:class:`anki_vector.util.Angle`: The y (vertical) field of view.""" + return self._fov_y + + class CameraComponent(util.Component): """Represents Vector's camera. @@ -167,6 +279,55 @@ def __init__(self, robot): self._latest_image_id: int = None self._camera_feed_task: asyncio.Task = None self._enabled = False + self._config = None # type CameraConfig + self._gain = 0.0 + self._exposure_ms = 0 + self._auto_exposure_enabled = True + + def set_config(self, message: protocol.CameraConfigRequest): + """Update Vector's camera configuration from the message sent from the Robot """ + self._config = CameraConfig.create_from_message(message) + + @connection.on_connection_thread(requires_control=False) + async def get_camera_config(self) -> protocol.CameraConfigResponse: + """ Get Vector's camera configuration + + Retrieves the calibrated camera settings. This is called during the Robot connection initialization, SDK + users should use the `config` property in most instances. + + :return: + """ + request = protocol.CameraConfigRequest() + return await self.conn.grpc_interface.GetCameraConfig(request) + + @property + def config(self) -> CameraConfig: + """:class:`anki_vector.camera.CameraConfig`: The read-only config/calibration for the camera""" + return self._config + + @property + def is_auto_exposure_enabled(self) -> bool: + """bool: True if auto exposure is currently enabled + + If auto exposure is enabled the `gain` and `exposure_ms` + values will constantly be updated by Vector. + """ + return self._auto_exposure_enabled + + @property + def gain(self) -> float: + """float: The current camera gain setting.""" + return self._gain + + @property + def exposure_ms(self) -> int: + """int: The current camera exposure setting in milliseconds.""" + return self._exposure_ms + + def update_state(self, _robot, _event_type, msg): + self._gain = msg.gain + self._exposure_ms = msg.exposure_ms + self._auto_exposure_enabled = msg.auto_exposure_enabled @property @util.block_while_none() @@ -330,7 +491,7 @@ async def _request_and_handle_images(self) -> None: self.logger.debug('Camera feed task was cancelled. This is expected during disconnection.') @connection.on_connection_thread() - async def capture_single_image(self) -> CameraImage: + async def capture_single_image(self, enable_high_resolution: bool = False) -> CameraImage: """Request to capture a single image from the robot's camera. This call requests the robot to capture an image and returns the @@ -346,10 +507,16 @@ async def capture_single_image(self) -> CameraImage: with anki_vector.Robot() as robot: image = robot.camera.capture_single_image() image.raw_image.show() + + :param enable_high_resolution: Enable/disable request for high resolution images. The default resolution + is 640x360, while the high resolution is 1280x720. """ if self._enabled: + self.logger.warning('Camera feed is enabled. Receiving image from the feed at default resolution.') return self._latest_image - req = protocol.CaptureSingleImageRequest() + if enable_high_resolution: + self.logger.warning('Capturing a high resolution (1280*720) image. Image events for this frame need to be scaled.') + req = protocol.CaptureSingleImageRequest(enable_high_resolution=enable_high_resolution) res = await self.grpc_interface.CaptureSingleImage(req) if res and res.data: image = _convert_to_pillow_image(res.data) @@ -357,6 +524,71 @@ async def capture_single_image(self) -> CameraImage: self.logger.error('Failed to capture a single image') + @connection.on_connection_thread() + async def enable_auto_exposure(self, enable_auto_exposure=True) -> protocol.SetCameraSettingsResponse: + """Enable auto exposure on Vector's Camera. + + Enable auto exposure on Vector's camera to constantly update the exposure + time and gain values based on the recent images. This is the default mode + when any SDK program starts. + + .. testcode:: + + import time + import anki_vector + with anki_vector.Robot() as robot: + robot.camera.enable_auto_exposure(False) + time.sleep(5) + + :param enable_auto_exposure: whether the camera should automatically adjust exposure + """ + + set_camera_settings_request = protocol.SetCameraSettingsRequest(enable_auto_exposure=enable_auto_exposure) + result = await self.conn.grpc_interface.SetCameraSettings(set_camera_settings_request) + self._auto_exposure_enabled = enable_auto_exposure + return result + + @connection.on_connection_thread() + async def set_manual_exposure(self, exposure_ms: int, gain: float) -> protocol.SetCameraSettingsResponse: + """Set manual exposure values for Vector's Camera. + + This will disable auto exposure on Vector's camera and force the specified exposure + time and gain values. + + .. testcode:: + + import time + import anki_vector + with anki_vector.Robot() as robot: + robot.camera.set_manual_exposure(1, 0.25) + time.sleep(5) + + :param exposure_ms: The desired exposure time in milliseconds. + Must be within the robot's exposure range from :attr:`CameraConfig.min_exposure_time_ms` to + :attr:`CameraConfig.max_exposure_time_ms` + :param gain: The desired gain value. + Must be within the robot's gain range from :attr:`CameraConfig.min_gain` to + :attr:`CameraConfig.max_gain` + Raises: + :class:`ValueError` if supplied an out-of-range exposure or gain + + """ + + if exposure_ms < self._config.min_exposure_time_ms \ + or exposure_ms > self._config.max_exposure_time_ms \ + or gain < self._config.min_gain \ + or gain > self._config.max_gain: + raise ValueError("Exposure settings out of range") + + set_camera_settings_request = protocol.SetCameraSettingsRequest(gain=gain, + exposure_ms=exposure_ms, + enable_auto_exposure=False) + result = await self.conn.grpc_interface.SetCameraSettings(set_camera_settings_request) + self._gain = gain + self._exposure_ms = exposure_ms + self._auto_exposure_enabled = False + return result + class EvtNewRawCameraImage: # pylint: disable=too-few-public-methods """Dispatched when a new raw image is received from the robot's camera. diff --git a/anki_vector/configure/__main__.py b/anki_vector/configure/__main__.py index 41c08cf..252cafe 100755 --- a/anki_vector/configure/__main__.py +++ b/anki_vector/configure/__main__.py @@ -30,6 +30,7 @@ import json import os from pathlib import Path +import platform import re import socket import sys @@ -66,7 +67,10 @@ class Api: def __init__(self): self._handler = ApiHandler( headers={ - 'User-Agent': f'Vector-sdk/{anki_vector.__version__}', + 'User-Agent': 'Vector-sdk/{} {}/{}'.format(anki_vector.__version__, + platform.python_implementation(), + platform.python_version()), + 'Anki-App-Key': 'aung2ieCho3aiph7Een3Ei' }, url='https://accounts.api.anki.com/1/sessions' diff --git a/anki_vector/connection.py b/anki_vector/connection.py index 472b0d8..86c0581 100644 --- a/anki_vector/connection.py +++ b/anki_vector/connection.py @@ -47,6 +47,14 @@ from .version import __version__ +class CancelType(Enum): + """Enum used to specify cancellation options for behaviors -- internal use only """ + #: Cancellable as an 'Action' + CANCELLABLE_ACTION = 0 + #: Cancellable as a 'Behavior' + CANCELLABLE_BEHAVIOR = 1 + + class ControlPriorityLevel(Enum): """Enum used to specify the priority level for the program.""" #: Runs above mandatory physical reactions, will drive off table, perform while on a slope, @@ -689,7 +697,7 @@ def _run_coroutine(self, coro): return asyncio.run_coroutine_threadsafe(coro, self._loop) -def on_connection_thread(log_messaging: bool = True, requires_control: bool = True, is_cancellable_behavior=False) -> Callable[[Coroutine[util.Component, Any, None]], Any]: +def on_connection_thread(log_messaging: bool = True, requires_control: bool = True, is_cancellable: CancelType = None) -> Callable[[Coroutine[util.Component, Any, None]], Any]: """A decorator generator used internally to denote which functions will run on the connection thread. This unblocks the caller of the wrapped function and allows them to continue running while the messages are being processed. @@ -706,7 +714,8 @@ async def on_connection_thread(self): :param log_messaging: True if the log output should include the entire message or just the size. Recommended for large binary return values. :param requires_control: True if the function should wait until behavior control is granted before executing. - :param is_cancellable_behavior: True if the behavior can be cancelled before it has completed. + :param is_cancellable: use a valid enum of :class:`CancelType` to specify the type of cancellation for the + function. Defaults to 'None' implying no support for responding to cancellation. :returns: A decorator which has 3 possible returns based on context: the result of the decorated function, the :class:`concurrent.futures.Future` which points to the decorated function, or the :class:`asyncio.Future` which points to the decorated function. @@ -769,10 +778,10 @@ def result(*args: List[Any], **kwargs: Dict[str, Any]) -> Any: # if the call supplies a _return_future parameter then override force_async with that. _return_future = kwargs.pop('_return_future', self.force_async) - behavior_id = None - if is_cancellable_behavior: - behavior_id = self._get_next_behavior_id() - kwargs['_behavior_id'] = behavior_id + action_id = None + if is_cancellable == CancelType.CANCELLABLE_ACTION: + action_id = self._get_next_action_id() + kwargs['_action_id'] = action_id wrapped_coroutine = log_handler(self.conn, func, self.logger, *args, **kwargs) @@ -783,15 +792,22 @@ def result(*args: List[Any], **kwargs: Dict[str, Any]) -> Any: "function '{}' is being invoked on that thread.\n".format(func.__name__ if hasattr(func, "__name__") else func)) future = asyncio.run_coroutine_threadsafe(wrapped_coroutine, self.conn.loop) - if is_cancellable_behavior: - def user_cancelled(fut): - if behavior_id is None: + if is_cancellable == CancelType.CANCELLABLE_ACTION: + def user_cancelled_action(fut): + if action_id is None: return if fut.cancelled(): - self._abort(behavior_id) + self._abort_action(action_id) + + future.add_done_callback(user_cancelled_action) + + if is_cancellable == CancelType.CANCELLABLE_BEHAVIOR: + def user_cancelled_behavior(fut): + if fut.cancelled(): + self._abort_behavior() - future.add_done_callback(user_cancelled) + future.add_done_callback(user_cancelled_behavior) if requires_control: self.conn.active_commands.append(future) diff --git a/anki_vector/events.py b/anki_vector/events.py index 259bc28..c847896 100644 --- a/anki_vector/events.py +++ b/anki_vector/events.py @@ -37,6 +37,7 @@ class Events(Enum): robot_state = "robot_state" #: Robot event containing changes to the robot's state. mirror_mode_disabled = "mirror_mode_disabled" # : Robot event triggered when mirror mode (camera feed displayed on robot's face) is automatically disabled due to SDK no longer having control of the robot. vision_modes_auto_disabled = "vision_modes_auto_disabled" # : Robot event triggered when all vision modes are automatically disabled due to the SDK no longer having control of the robot. + camera_settings_update = "camera_settings_update" # : Robot event triggered when camera exposure settings change # Objects object_available = "object_available" #: After the ConnectCube process is started, all available light cubes in range will broadcast an availability message through the Robot. @@ -48,16 +49,23 @@ class Events(Enum): robot_observed_object = "robot_observed_object" #: Robot event triggered when an object is observed by the robot. cube_connection_lost = "cube_connection_lost" #: Robot event triggered when an object's subscribed connection has been lost. - robot_observed_face = "robot_observed_face" #: Robot event for when a face is observed by the robot. - robot_changed_observed_face_id = "robot_changed_observed_face_id" # : Robot event for when a known face changes its id. + robot_observed_motion = "robot_observed_motion" #: Robot event dispatched when Vector observes motion. + robot_observed_face = "robot_observed_face" #: Robot event for when a face is observed by the robot. + robot_changed_observed_face_id = "robot_changed_observed_face_id" #: Robot event for when a known face changes its id. + robot_erased_enrolled_face = "robot_erased_enrolled_face" #: Robot event for when an enrolled face has been removed from the robot. + robot_renamed_enrolled_face = "robot_renamed_enrolled_face" #: Robot event for when a known face changes its name. + unexpected_movement = "unexpected_movement" #: Robot event for when it detects movement that does not match what motors were commanded (e.g. turning in the wrong direction) - wake_word = "wake_word" #: Robot event triggered when Vector hears "Hey Vector" - user_intent = "user_intent" #: Robot event triggered after Vector processes voice commands + wake_word = "wake_word" #: Robot event triggered when Vector hears "Hey Vector". + user_intent = "user_intent" #: Robot event triggered after Vector processes voice commands. # Audio audio_send_mode_changed = "audio_send_mode_changed" #: Robot event containing changes to the robot's audio stream source data processing mode. # Generated by SDK + face_observed = "face_observed" #: Python event triggered in response to robot_observed_face with sdk metadata. + face_appeared = "face_appeared" #: Python event triggered when an face first receives robot_observed_face. + face_disappeared = "face_disappeared" #: Python event triggered when an face has not received a robot_observed_face for a specified time. object_observed = "object_observed" #: Python event triggered in response to robot_observed_object with sdk metadata. object_appeared = "object_appeared" #: Python event triggered when an object first receives robot_observed_object. object_disappeared = "object_disappeared" #: Python event triggered when an object has not received a robot_observed_object for a specified time. diff --git a/anki_vector/exceptions.py b/anki_vector/exceptions.py index 309ddbf..83548a6 100644 --- a/anki_vector/exceptions.py +++ b/anki_vector/exceptions.py @@ -24,6 +24,7 @@ __all__ = ['VectorAsyncException', 'VectorBehaviorControlException', 'VectorCameraFeedException', + 'VectorCameraImageCaptureException', 'VectorConfigurationException', 'VectorConnectionException', 'VectorControlException', @@ -144,6 +145,10 @@ class VectorCameraFeedException(_VectorGenericException): Make sure to enable the camera feed either using Robot(show_viewer=True), or robot.camera.init_camera_feed()""" +class VectorCameraImageCaptureException(_VectorGenericException): + """Image capture exception.""" + + class VectorConfigurationException(_VectorGenericException): """Invalid or missing configuration data.""" diff --git a/anki_vector/faces.py b/anki_vector/faces.py index d8d0e6f..49a680c 100644 --- a/anki_vector/faces.py +++ b/anki_vector/faces.py @@ -22,15 +22,22 @@ Each face is assigned a :class:`Face` object, which generates a number of observable events whenever the face is observed or when the face id is updated. + +Faces can generate events which can be subscribed to from the anki_vector.events +class, such as face_appeared (of type EvtFaceAppeared), and face_disappeared (of +type EvtFaceDisappeared), which are broadcast based on both robot originating +events and local state. """ # __all__ should order by constants, event classes, other classes, functions. -__all__ = ['Expression', 'Face', 'FaceComponent'] +__all__ = ['FACE_VISIBILITY_TIMEOUT', 'EvtFaceAppeared', 'EvtFaceDisappeared', + 'EvtFaceObserved', 'Expression', 'Face', 'FaceComponent'] from enum import Enum from typing import List -from . import connection, util, objects, events +from . import connection, events, util, objects +from .events import Events from .messaging import protocol #: Length of time in seconds to go without receiving an observed event before @@ -198,6 +205,8 @@ class Expression(Enum): #: Facial expression sadness SADNESS = protocol.FacialExpression.Value("EXPRESSION_SADNESS") +# TODO Review this file and add pytests as is reasonable, like name_face (requires a face object), request_enrolled_names, maybe update_enrolled_face_by_id, etc. + class Face(objects.ObservableObject): """A single face that Vector has detected. @@ -210,6 +219,10 @@ class Face(objects.ObservableObject): which face he is looking at. """ + #: Length of time in seconds to go without receiving an observed event before + #: assuming that Vector can no longer see a face. + visibility_timeout = FACE_VISIBILITY_TIMEOUT + def __init__(self, robot, pose: util.Pose, @@ -255,6 +268,56 @@ def __repr__(self): f"Updated face id: {self.updated_face_id} Name: {self.name} " f"Expression: {protocol.FacialExpression.Name(self.expression)}>") + @connection.on_connection_thread(requires_control=False) + async def name_face(self, name: str) -> protocol.EnrollFaceResponse: + """Request to enroll this face with a name. Vector will remember this name between SDK runs. + + Triggers Vector to run his animation that scans faces and use the camera feed to store the face. + + While enrolling a face, make sure to look at Vector straight-on during the enrollment from about 1.5 to 2 feet away. + + :param name: The name that will be assigned to this face. + + .. testcode:: + + import anki_vector + from anki_vector.util import degrees + + with anki_vector.Robot(enable_face_detection=True) as robot: + # If necessary, move Vector's Head and Lift to make it easy to see his face + robot.behavior.set_head_angle(degrees(45.0)) + robot.behavior.set_lift_height(0.0) + + # TODO Replace with wait_for_observed_face + face = None + for face in robot.world.visible_faces: + break + + if face is None: + print("--- No face found ---") + else: + print("--- Existing Face attributes ---") + print(f"Visible face name: {face.name}") + print(f"Visible face id: {face.face_id}") + + # Name this face "Boris" + face.name_face("Boris") + + print(f"{robot.faces.request_enrolled_names()}") + """ + self.logger.info("Enrolling face=%s with name '%s'", self, name) + + req = protocol.SetFaceToEnrollRequest(name=name, + observed_id=self.face_id, + save_id=0, # must be 0 if self.face_id doesn't already have a name + save_to_robot=True, + say_name=False, + use_music=False) + await self.grpc_interface.SetFaceToEnroll(req) + + enroll_face_request = protocol.EnrollFaceRequest() + return await self.grpc_interface.EnrollFace(enroll_face_request) + def teardown(self): """All faces will be torn down by the world when no longer needed.""" self._robot.events.unsubscribe(self._on_face_observed, @@ -631,6 +694,15 @@ def test_subscriber(robot, event_type, event): #### Private Event Handlers #### + def _dispatch_observed_event(self, image_rect): + self.conn.run_soon(self._robot.events.dispatch_event(EvtFaceObserved(self, image_rect=image_rect, name=self._name, pose=self._pose), Events.face_observed)) + + def _dispatch_appeared_event(self, image_rect): + self.conn.run_soon(self._robot.events.dispatch_event(EvtFaceAppeared(self, image_rect=image_rect, name=self._name, pose=self._pose), Events.face_appeared)) + + def _dispatch_disappeared_event(self): + self.conn.run_soon(self._robot.events.dispatch_event(EvtFaceDisappeared(self), Events.face_disappeared)) + def _on_face_observed(self, _robot, _event_type, msg): """Unpacks the face observed stream data from Vector into a Face instance.""" if self._face_id == msg.face_id: @@ -665,7 +737,7 @@ class FaceComponent(util.Component): """Manage the state of the faces on the robot.""" @connection.on_connection_thread(requires_control=False) - async def request_enrolled_names(self) -> protocol.RequestEnrolledNamesRequest: + async def request_enrolled_names(self) -> protocol.RequestEnrolledNamesResponse: """Asks the robot for the list of names attached to faces that it can identify. .. testcode:: @@ -691,7 +763,11 @@ async def update_enrolled_face_by_id(self, face_id: int, old_name: str, new_name import anki_vector + def on_robot_renamed_enrolled_face(robot, event_type, event): + print(f"----Face has been renamed on robot. Event: {event_type} = {event}----") + with anki_vector.Robot() as robot: + robot.events.subscribe(on_robot_renamed_enrolled_face, Events.robot_renamed_enrolled_face) robot.faces.update_enrolled_face_by_id(1, 'Hanns', 'Boris') """ req = protocol.UpdateEnrolledFaceByIDRequest(face_id=face_id, @@ -706,10 +782,24 @@ async def erase_enrolled_face_by_id(self, face_id: int): .. testcode:: + import time import anki_vector + from anki_vector.events import Events + + def on_robot_erased_enrolled_face(robot, event_type, event): + print(f"Face has been erased from robot. Event: {event_type} = {event}") with anki_vector.Robot() as robot: - robot.faces.erase_enrolled_face_by_id(1) + robot.events.subscribe(on_robot_erased_enrolled_face, Events.robot_erased_enrolled_face) + + name_data_list = robot.faces.request_enrolled_names() + print(f"Enrolled names: {name_data_list}") + + # Deletes all enrolled faces from Vector. Use with care! + for face in name_data_list.faces: + robot.faces.erase_enrolled_face_by_id(face.face_id) + + time.sleep(3) """ req = protocol.EraseEnrolledFaceByIDRequest(face_id=face_id) return await self.grpc_interface.EraseEnrolledFaceByID(req) diff --git a/anki_vector/messaging/alexa.proto b/anki_vector/messaging/alexa.proto index d4397d7..b780c4e 100644 --- a/anki_vector/messaging/alexa.proto +++ b/anki_vector/messaging/alexa.proto @@ -18,4 +18,41 @@ syntax = "proto3"; package Anki.Vector.external_interface; +import "anki_vector/messaging/response_status.proto"; + +enum AlexaAuthState { + // Invalid/error/versioning issue + ALEXA_AUTH_INVALID = 0; + + // Not opted in, or opt-in attempted but failed + ALEXA_AUTH_UNINITIALIZED = 1; + // Opted in, and attempting to authorize + ALEXA_AUTH_REQUESTING_AUTH = 2; + // Opted in, and waiting on the user to enter a code + ALEXA_AUTH_WAITING_FOR_CODE = 3; + // Opted in, and authorized / in use + ALEXA_AUTH_AUTHORIZED = 4; +} + +message AlexaAuthStateRequest { +} + +message AlexaAuthStateResponse { + ResponseStatus status = 1; + AlexaAuthState auth_state = 2; + string extra = 3; +} + +message AlexaOptInRequest { + bool opt_in = 1; +} + +message AlexaOptInResponse{ + ResponseStatus status = 1; +} + +message AlexaAuthEvent { + AlexaAuthState auth_state = 1; + string extra = 2; +} diff --git a/anki_vector/messaging/external_interface.proto b/anki_vector/messaging/external_interface.proto index aeaedb3..c1809fd 100644 --- a/anki_vector/messaging/external_interface.proto +++ b/anki_vector/messaging/external_interface.proto @@ -19,11 +19,13 @@ syntax = "proto3"; package Anki.Vector.external_interface; import "google/api/annotations.proto"; +import "anki_vector/messaging/alexa.proto"; import "anki_vector/messaging/behavior.proto"; import "anki_vector/messaging/cube.proto"; import "anki_vector/messaging/messages.proto"; import "anki_vector/messaging/nav_map.proto"; import "anki_vector/messaging/shared.proto"; +import "anki_vector/messaging/settings.proto"; //

The valid versions of the protocol.

//

Protocol versions are updated when messages change significantly: @@ -171,6 +173,14 @@ service ExternalInterface { }; } + // Enroll a face. Must be used with SetFaceToEnroll (v1.7) + rpc EnrollFace(EnrollFaceRequest) returns (EnrollFaceResponse) { + option (google.api.http) = { + post: "/v1/enroll_face", + body: "*" + }; + } + rpc EnableMarkerDetection(EnableMarkerDetectionRequest) returns (EnableMarkerDetectionResponse) { option (google.api.http) = { post: "/v1/enable_marker_detection", @@ -221,6 +231,14 @@ service ExternalInterface { }; } + // Cancel running SDK Behavior (v1.7) + rpc CancelBehavior(CancelBehaviorRequest) returns (CancelBehaviorResponse) { + option (google.api.http) = { + post: "/v1/cancel_behavior", + body: "*" + }; + } + // Tells Vector to drive to the specified pose and orientation. rpc GoToPose(GoToPoseRequest) returns (GoToPoseResponse) { option (google.api.http) = { @@ -480,6 +498,14 @@ service ExternalInterface { }; } + // Get Vector's camera configuration. (v1.7) + rpc GetCameraConfig(CameraConfigRequest) returns (CameraConfigResponse) { + option (google.api.http) = { + post: "/v1/get_camera_config", + body: "*" + }; + } + // Set Vector's eye color. rpc SetEyeColor(SetEyeColorRequest) returns (SetEyeColorResponse) { option (google.api.http) = { @@ -496,4 +522,109 @@ service ExternalInterface { }; } -} \ No newline at end of file + // Set Vector's camera settings (v1.7) + rpc SetCameraSettings(SetCameraSettingsRequest) returns (SetCameraSettingsResponse) { + option (google.api.http) = { + post: "/v1/set_camera_settings", + body: "*" + }; + } + + rpc AppIntent(AppIntentRequest) returns (AppIntentResponse) { + option (google.api.http) = { + post: "/v1/app_intent", + body: "*" + }; + } + + // Update settings + // Added by wayne@codaris.com + rpc UpdateSettings(UpdateSettingsRequest) returns (UpdateSettingsResponse) { + option (google.api.http) = { + post: "/v1/update_settings", + body: "*" + }; + } + + rpc GetLatestAttentionTransfer(LatestAttentionTransferRequest) returns (LatestAttentionTransferResponse) { + option (google.api.http) = { + post: "/v1/get_latest_attention_transfer", + body: "*" + }; + } + + // Pull Jdocs + // Added by wayne@codaris.com + rpc PullJdocs(PullJdocsRequest) returns (PullJdocsResponse) { + option (google.api.http) = { + post: "/v1/pull_jdocs", + body: "*" + }; + } + + rpc UpdateAccountSettings(UpdateAccountSettingsRequest) returns (UpdateAccountSettingsResponse) { + option (google.api.http) = { + post: "/v1/update_account_settings", + body: "*" + }; + } + + // StartUpdateEngine cycles the update-engine service (to start a new check for an update) and sets up a stream of + // UpdateStatusResponse Events. + rpc StartUpdateEngine(CheckUpdateStatusRequest) returns (CheckUpdateStatusResponse) { + option (google.api.http) = { + post: "/v1/start_update_engine", + body: "*" + }; + } + + // CheckUpdateStatus tells if the robot is ready to reboot and update. + rpc CheckUpdateStatus(CheckUpdateStatusRequest) returns (CheckUpdateStatusResponse) { + option (google.api.http) = { + post: "/v1/check_update_status", + body: "*" + }; + } + + rpc UpdateAndRestart(UpdateAndRestartRequest) returns (UpdateAndRestartResponse) { + option (google.api.http) = { + post: "/v1/update_and_restart", + body: "*" + }; + } + + rpc CheckCloudConnection(CheckCloudRequest) returns (CheckCloudResponse) { + option (google.api.http) = { + post: "/v1/check_cloud_connection", + body: "*" + }; + } + + rpc GetFeatureFlag(FeatureFlagRequest) returns (FeatureFlagResponse) { + option (google.api.http) = { + post: "/v1/feature_flag", + body: "*" + }; + } + + rpc GetFeatureFlagList(FeatureFlagListRequest) returns (FeatureFlagListResponse) { + option (google.api.http) = { + post: "/v1/feature_flag_list", + body: "*" + }; + } + + rpc GetAlexaAuthState(AlexaAuthStateRequest) returns (AlexaAuthStateResponse) { + option (google.api.http) = { + post: "/v1/alexa_auth_state", + body: "*" + }; + } + + rpc AlexaOptIn(AlexaOptInRequest) returns (AlexaOptInResponse) { + option (google.api.http) = { + post: "/v1/alexa_opt_in", + body: "*" + }; + } +} diff --git a/anki_vector/messaging/external_interface_pb2.py b/anki_vector/messaging/external_interface_pb2.py index 1a9e771..efe6c17 100644 --- a/anki_vector/messaging/external_interface_pb2.py +++ b/anki_vector/messaging/external_interface_pb2.py @@ -26,7 +26,7 @@ name='anki_vector/messaging/external_interface.proto', package='Anki.Vector.external_interface', syntax='proto3', - serialized_pb=_b('\n.anki_vector/messaging/external_interface.proto\x12\x1e\x41nki.Vector.external_interface\x1a\x1cgoogle/api/annotations.proto\x1a$anki_vector/messaging/behavior.proto\x1a anki_vector/messaging/cube.proto\x1a$anki_vector/messaging/messages.proto\x1a#anki_vector/messaging/nav_map.proto\x1a\"anki_vector/messaging/shared.proto*o\n\x0fProtocolVersion\x12\x1c\n\x18PROTOCOL_VERSION_UNKNOWN\x10\x00\x12\x1c\n\x18PROTOCOL_VERSION_MINIMUM\x10\x00\x12\x1c\n\x18PROTOCOL_VERSION_CURRENT\x10\x05\x1a\x02\x10\x01\x32\xddT\n\x11\x45xternalInterface\x12\xa3\x01\n\x0fProtocolVersion\x12\x36.Anki.Vector.external_interface.ProtocolVersionRequest\x1a\x37.Anki.Vector.external_interface.ProtocolVersionResponse\"\x1f\x82\xd3\xe4\x93\x02\x19\"\x14/v1/protocol_version:\x01*\x12\xab\x01\n\x11SDKInitialization\x12\x38.Anki.Vector.external_interface.SDKInitializationRequest\x1a\x39.Anki.Vector.external_interface.SDKInitializationResponse\"!\x82\xd3\xe4\x93\x02\x1b\"\x16/v1/sdk_initialization:\x01*\x12x\n\x0b\x44riveWheels\x12\x32.Anki.Vector.external_interface.DriveWheelsRequest\x1a\x33.Anki.Vector.external_interface.DriveWheelsResponse\"\x00\x12\x8c\x01\n\x14PlayAnimationTrigger\x12;.Anki.Vector.external_interface.PlayAnimationTriggerRequest\x1a\x35.Anki.Vector.external_interface.PlayAnimationResponse\"\x00\x12~\n\rPlayAnimation\x12\x34.Anki.Vector.external_interface.PlayAnimationRequest\x1a\x35.Anki.Vector.external_interface.PlayAnimationResponse\"\x00\x12\x9f\x01\n\x0eListAnimations\x12\x35.Anki.Vector.external_interface.ListAnimationsRequest\x1a\x36.Anki.Vector.external_interface.ListAnimationsResponse\"\x1e\x82\xd3\xe4\x93\x02\x18\"\x13/v1/list_animations:\x01*\x12\xbc\x01\n\x15ListAnimationTriggers\x12<.Anki.Vector.external_interface.ListAnimationTriggersRequest\x1a=.Anki.Vector.external_interface.ListAnimationTriggersResponse\"&\x82\xd3\xe4\x93\x02 \"\x1b/v1/list_animation_triggers:\x01*\x12o\n\x08MoveHead\x12/.Anki.Vector.external_interface.MoveHeadRequest\x1a\x30.Anki.Vector.external_interface.MoveHeadResponse\"\x00\x12o\n\x08MoveLift\x12/.Anki.Vector.external_interface.MoveLiftRequest\x1a\x30.Anki.Vector.external_interface.MoveLiftResponse\"\x00\x12~\n\rStopAllMotors\x12\x34.Anki.Vector.external_interface.StopAllMotorsRequest\x1a\x35.Anki.Vector.external_interface.StopAllMotorsResponse\"\x00\x12\xb5\x01\n\x13\x44isplayFaceImageRGB\x12:.Anki.Vector.external_interface.DisplayFaceImageRGBRequest\x1a;.Anki.Vector.external_interface.DisplayFaceImageRGBResponse\"%\x82\xd3\xe4\x93\x02\x1f\"\x1a/v1/display_face_image_rgb:\x01*\x12\x9d\x01\n\x0b\x45ventStream\x12,.Anki.Vector.external_interface.EventRequest\x1a-.Anki.Vector.external_interface.EventResponse\"/\x82\xd3\xe4\x93\x02)\"\x10/v1/event_stream:\x01*Z\x12\x12\x10/v1/event_stream0\x01\x12\x9c\x01\n\x1b\x45xternalAudioStreamPlayback\x12:.Anki.Vector.external_interface.ExternalAudioStreamRequest\x1a;.Anki.Vector.external_interface.ExternalAudioStreamResponse\"\x00(\x01\x30\x01\x12\x88\x01\n\x0f\x42\x65haviorControl\x12\x36.Anki.Vector.external_interface.BehaviorControlRequest\x1a\x37.Anki.Vector.external_interface.BehaviorControlResponse\"\x00(\x01\x30\x01\x12\xb2\x01\n\x15\x41ssumeBehaviorControl\x12\x36.Anki.Vector.external_interface.BehaviorControlRequest\x1a\x37.Anki.Vector.external_interface.BehaviorControlResponse\"&\x82\xd3\xe4\x93\x02 \"\x1b/v1/assume_behavior_control:\x01*0\x01\x12\xb8\x01\n\x14\x43\x61ncelFaceEnrollment\x12;.Anki.Vector.external_interface.CancelFaceEnrollmentRequest\x1a<.Anki.Vector.external_interface.CancelFaceEnrollmentResponse\"%\x82\xd3\xe4\x93\x02\x1f\"\x1a/v1/cancel_face_enrollment:\x01*\x12\xb8\x01\n\x14RequestEnrolledNames\x12;.Anki.Vector.external_interface.RequestEnrolledNamesRequest\x1a<.Anki.Vector.external_interface.RequestEnrolledNamesResponse\"%\x82\xd3\xe4\x93\x02\x1f\"\x1a/v1/request_enrolled_names:\x01*\x12\xc2\x01\n\x16UpdateEnrolledFaceByID\x12=.Anki.Vector.external_interface.UpdateEnrolledFaceByIDRequest\x1a>.Anki.Vector.external_interface.UpdateEnrolledFaceByIDResponse\")\x82\xd3\xe4\x93\x02#\"\x1e/v1/update_enrolled_face_by_id:\x01*\x12\xbe\x01\n\x15\x45raseEnrolledFaceByID\x12<.Anki.Vector.external_interface.EraseEnrolledFaceByIDRequest\x1a=.Anki.Vector.external_interface.EraseEnrolledFaceByIDResponse\"(\x82\xd3\xe4\x93\x02\"\"\x1d/v1/erase_enrolled_face_by_id:\x01*\x12\xbd\x01\n\x15\x45raseAllEnrolledFaces\x12<.Anki.Vector.external_interface.EraseAllEnrolledFacesRequest\x1a=.Anki.Vector.external_interface.EraseAllEnrolledFacesResponse\"\'\x82\xd3\xe4\x93\x02!\"\x1c/v1/erase_all_enrolled_faces:\x01*\x12\xa5\x01\n\x0fSetFaceToEnroll\x12\x36.Anki.Vector.external_interface.SetFaceToEnrollRequest\x1a\x37.Anki.Vector.external_interface.SetFaceToEnrollResponse\"!\x82\xd3\xe4\x93\x02\x1b\"\x16/v1/set_face_to_enroll:\x01*\x12\xbc\x01\n\x15\x45nableMarkerDetection\x12<.Anki.Vector.external_interface.EnableMarkerDetectionRequest\x1a=.Anki.Vector.external_interface.EnableMarkerDetectionResponse\"&\x82\xd3\xe4\x93\x02 \"\x1b/v1/enable_marker_detection:\x01*\x12\xb4\x01\n\x13\x45nableFaceDetection\x12:.Anki.Vector.external_interface.EnableFaceDetectionRequest\x1a;.Anki.Vector.external_interface.EnableFaceDetectionResponse\"$\x82\xd3\xe4\x93\x02\x1e\"\x19/v1/enable_face_detection:\x01*\x12\xbc\x01\n\x15\x45nableMotionDetection\x12<.Anki.Vector.external_interface.EnableMotionDetectionRequest\x1a=.Anki.Vector.external_interface.EnableMotionDetectionResponse\"&\x82\xd3\xe4\x93\x02 \"\x1b/v1/enable_motion_detection:\x01*\x12\xa8\x01\n\x10\x45nableMirrorMode\x12\x37.Anki.Vector.external_interface.EnableMirrorModeRequest\x1a\x38.Anki.Vector.external_interface.EnableMirrorModeResponse\"!\x82\xd3\xe4\x93\x02\x1b\"\x16/v1/enable_mirror_mode:\x01*\x12\xb8\x01\n\x14\x45nableImageStreaming\x12;.Anki.Vector.external_interface.EnableImageStreamingRequest\x1a<.Anki.Vector.external_interface.EnableImageStreamingResponse\"%\x82\xd3\xe4\x93\x02\x1f\"\x1a/v1/enable_image_streaming:\x01*\x12\xc5\x01\n\x17IsImageStreamingEnabled\x12>.Anki.Vector.external_interface.IsImageStreamingEnabledRequest\x1a?.Anki.Vector.external_interface.IsImageStreamingEnabledResponse\")\x82\xd3\xe4\x93\x02#\"\x1e/v1/is_image_streaming_enabled:\x01*\x12\xb6\x01\n\x13\x43\x61ncelActionByIdTag\x12:.Anki.Vector.external_interface.CancelActionByIdTagRequest\x1a;.Anki.Vector.external_interface.CancelActionByIdTagResponse\"&\x82\xd3\xe4\x93\x02 \"\x1b/v1/cancel_action_by_id_tag:\x01*\x12\x88\x01\n\x08GoToPose\x12/.Anki.Vector.external_interface.GoToPoseRequest\x1a\x30.Anki.Vector.external_interface.GoToPoseResponse\"\x19\x82\xd3\xe4\x93\x02\x13\"\x0e/v1/go_to_pose:\x01*\x12\x98\x01\n\x0c\x44ockWithCube\x12\x33.Anki.Vector.external_interface.DockWithCubeRequest\x1a\x34.Anki.Vector.external_interface.DockWithCubeResponse\"\x1d\x82\xd3\xe4\x93\x02\x17\"\x12/v1/dock_with_cube:\x01*\x12\xa4\x01\n\x0f\x44riveOffCharger\x12\x36.Anki.Vector.external_interface.DriveOffChargerRequest\x1a\x37.Anki.Vector.external_interface.DriveOffChargerResponse\" \x82\xd3\xe4\x93\x02\x1a\"\x15/v1/drive_off_charger:\x01*\x12\xa0\x01\n\x0e\x44riveOnCharger\x12\x35.Anki.Vector.external_interface.DriveOnChargerRequest\x1a\x36.Anki.Vector.external_interface.DriveOnChargerResponse\"\x1f\x82\xd3\xe4\x93\x02\x19\"\x14/v1/drive_on_charger:\x01*\x12\x8b\x01\n\tFindFaces\x12\x30.Anki.Vector.external_interface.FindFacesRequest\x1a\x31.Anki.Vector.external_interface.FindFacesResponse\"\x19\x82\xd3\xe4\x93\x02\x13\"\x0e/v1/find_faces:\x01*\x12\xad\x01\n\x11LookAroundInPlace\x12\x38.Anki.Vector.external_interface.LookAroundInPlaceRequest\x1a\x39.Anki.Vector.external_interface.LookAroundInPlaceResponse\"#\x82\xd3\xe4\x93\x02\x1d\"\x18/v1/look_around_in_place:\x01*\x12\x8b\x01\n\tRollBlock\x12\x30.Anki.Vector.external_interface.RollBlockRequest\x1a\x31.Anki.Vector.external_interface.RollBlockResponse\"\x19\x82\xd3\xe4\x93\x02\x13\"\x0e/v1/roll_block:\x01*\x12\x8f\x01\n\nPhotosInfo\x12\x31.Anki.Vector.external_interface.PhotosInfoRequest\x1a\x32.Anki.Vector.external_interface.PhotosInfoResponse\"\x1a\x82\xd3\xe4\x93\x02\x14\"\x0f/v1/photos_info:\x01*\x12z\n\x05Photo\x12,.Anki.Vector.external_interface.PhotoRequest\x1a-.Anki.Vector.external_interface.PhotoResponse\"\x14\x82\xd3\xe4\x93\x02\x0e\"\t/v1/photo:\x01*\x12\x8a\x01\n\tThumbnail\x12\x30.Anki.Vector.external_interface.ThumbnailRequest\x1a\x31.Anki.Vector.external_interface.ThumbnailResponse\"\x18\x82\xd3\xe4\x93\x02\x12\"\r/v1/thumbnail:\x01*\x12\x93\x01\n\x0b\x44\x65letePhoto\x12\x32.Anki.Vector.external_interface.DeletePhotoRequest\x1a\x33.Anki.Vector.external_interface.DeletePhotoResponse\"\x1b\x82\xd3\xe4\x93\x02\x15\"\x10/v1/delete_photo:\x01*\x12~\n\rDriveStraight\x12\x34.Anki.Vector.external_interface.DriveStraightRequest\x1a\x35.Anki.Vector.external_interface.DriveStraightResponse\"\x00\x12x\n\x0bTurnInPlace\x12\x32.Anki.Vector.external_interface.TurnInPlaceRequest\x1a\x33.Anki.Vector.external_interface.TurnInPlaceResponse\"\x00\x12{\n\x0cSetHeadAngle\x12\x33.Anki.Vector.external_interface.SetHeadAngleRequest\x1a\x34.Anki.Vector.external_interface.SetHeadAngleResponse\"\x00\x12~\n\rSetLiftHeight\x12\x34.Anki.Vector.external_interface.SetLiftHeightRequest\x1a\x35.Anki.Vector.external_interface.SetLiftHeightResponse\"\x00\x12\x84\x01\n\x0fTurnTowardsFace\x12\x36.Anki.Vector.external_interface.TurnTowardsFaceRequest\x1a\x37.Anki.Vector.external_interface.TurnTowardsFaceResponse\"\x00\x12u\n\nGoToObject\x12\x31.Anki.Vector.external_interface.GoToObjectRequest\x1a\x32.Anki.Vector.external_interface.GoToObjectResponse\"\x00\x12u\n\nRollObject\x12\x31.Anki.Vector.external_interface.RollObjectRequest\x1a\x32.Anki.Vector.external_interface.RollObjectResponse\"\x00\x12x\n\x0bPopAWheelie\x12\x32.Anki.Vector.external_interface.PopAWheelieRequest\x1a\x33.Anki.Vector.external_interface.PopAWheelieResponse\"\x00\x12{\n\x0cPickupObject\x12\x33.Anki.Vector.external_interface.PickupObjectRequest\x1a\x34.Anki.Vector.external_interface.PickupObjectResponse\"\x00\x12\x9c\x01\n\x17PlaceObjectOnGroundHere\x12>.Anki.Vector.external_interface.PlaceObjectOnGroundHereRequest\x1a?.Anki.Vector.external_interface.PlaceObjectOnGroundHereResponse\"\x00\x12~\n\x0fSetMasterVolume\x12\x33.Anki.Vector.external_interface.MasterVolumeRequest\x1a\x34.Anki.Vector.external_interface.MasterVolumeResponse\"\x00\x12\xaf\x01\n\x12UserAuthentication\x12\x39.Anki.Vector.external_interface.UserAuthenticationRequest\x1a:.Anki.Vector.external_interface.UserAuthenticationResponse\"\"\x82\xd3\xe4\x93\x02\x1c\"\x17/v1/user_authentication:\x01*\x12\x97\x01\n\x0c\x42\x61tteryState\x12\x33.Anki.Vector.external_interface.BatteryStateRequest\x1a\x34.Anki.Vector.external_interface.BatteryStateResponse\"\x1c\x82\xd3\xe4\x93\x02\x16\"\x11/v1/battery_state:\x01*\x12\x97\x01\n\x0cVersionState\x12\x33.Anki.Vector.external_interface.VersionStateRequest\x1a\x34.Anki.Vector.external_interface.VersionStateResponse\"\x1c\x82\xd3\xe4\x93\x02\x16\"\x11/v1/version_state:\x01*\x12\x83\x01\n\x07SayText\x12..Anki.Vector.external_interface.SayTextRequest\x1a/.Anki.Vector.external_interface.SayTextResponse\"\x17\x82\xd3\xe4\x93\x02\x11\"\x0c/v1/say_text:\x01*\x12\x93\x01\n\x0b\x43onnectCube\x12\x32.Anki.Vector.external_interface.ConnectCubeRequest\x1a\x33.Anki.Vector.external_interface.ConnectCubeResponse\"\x1b\x82\xd3\xe4\x93\x02\x15\"\x10/v1/connect_cube:\x01*\x12\x9f\x01\n\x0e\x44isconnectCube\x12\x35.Anki.Vector.external_interface.DisconnectCubeRequest\x1a\x36.Anki.Vector.external_interface.DisconnectCubeResponse\"\x1e\x82\xd3\xe4\x93\x02\x18\"\x13/v1/disconnect_cube:\x01*\x12\x9f\x01\n\x0e\x43ubesAvailable\x12\x35.Anki.Vector.external_interface.CubesAvailableRequest\x1a\x36.Anki.Vector.external_interface.CubesAvailableResponse\"\x1e\x82\xd3\xe4\x93\x02\x18\"\x13/v1/cubes_available:\x01*\x12\xa4\x01\n\x0f\x46lashCubeLights\x12\x36.Anki.Vector.external_interface.FlashCubeLightsRequest\x1a\x37.Anki.Vector.external_interface.FlashCubeLightsResponse\" \x82\xd3\xe4\x93\x02\x1a\"\x15/v1/flash_cube_lights:\x01*\x12\xb4\x01\n\x13\x46orgetPreferredCube\x12:.Anki.Vector.external_interface.ForgetPreferredCubeRequest\x1a;.Anki.Vector.external_interface.ForgetPreferredCubeResponse\"$\x82\xd3\xe4\x93\x02\x1e\"\x19/v1/forget_preferred_cube:\x01*\x12\xa8\x01\n\x10SetPreferredCube\x12\x37.Anki.Vector.external_interface.SetPreferredCubeRequest\x1a\x38.Anki.Vector.external_interface.SetPreferredCubeResponse\"!\x82\xd3\xe4\x93\x02\x1b\"\x16/v1/set_preferred_cube:\x01*\x12\xb4\x01\n\x13\x44\x65leteCustomObjects\x12:.Anki.Vector.external_interface.DeleteCustomObjectsRequest\x1a;.Anki.Vector.external_interface.DeleteCustomObjectsResponse\"$\x82\xd3\xe4\x93\x02\x1e\"\x19/v1/delete_custom_objects:\x01*\x12\xc5\x01\n\x17\x43reateFixedCustomObject\x12>.Anki.Vector.external_interface.CreateFixedCustomObjectRequest\x1a?.Anki.Vector.external_interface.CreateFixedCustomObjectResponse\")\x82\xd3\xe4\x93\x02#\"\x1e/v1/create_fixed_custom_object:\x01*\x12\xb0\x01\n\x12\x44\x65\x66ineCustomObject\x12\x39.Anki.Vector.external_interface.DefineCustomObjectRequest\x1a:.Anki.Vector.external_interface.DefineCustomObjectResponse\"#\x82\xd3\xe4\x93\x02\x1d\"\x18/v1/define_custom_object:\x01*\x12~\n\rSetCubeLights\x12\x34.Anki.Vector.external_interface.SetCubeLightsRequest\x1a\x35.Anki.Vector.external_interface.SetCubeLightsResponse\"\x00\x12\x8d\x01\n\tAudioFeed\x12\x30.Anki.Vector.external_interface.AudioFeedRequest\x1a\x31.Anki.Vector.external_interface.AudioFeedResponse\"\x19\x82\xd3\xe4\x93\x02\x13\"\x0e/v1/audio_feed:\x01*0\x01\x12\x91\x01\n\nCameraFeed\x12\x31.Anki.Vector.external_interface.CameraFeedRequest\x1a\x32.Anki.Vector.external_interface.CameraFeedResponse\"\x1a\x82\xd3\xe4\x93\x02\x14\"\x0f/v1/camera_feed:\x01*0\x01\x12\xb0\x01\n\x12\x43\x61ptureSingleImage\x12\x39.Anki.Vector.external_interface.CaptureSingleImageRequest\x1a:.Anki.Vector.external_interface.CaptureSingleImageResponse\"#\x82\xd3\xe4\x93\x02\x1d\"\x18/v1/capture_single_image:\x01*\x12\x94\x01\n\x0bSetEyeColor\x12\x32.Anki.Vector.external_interface.SetEyeColorRequest\x1a\x33.Anki.Vector.external_interface.SetEyeColorResponse\"\x1c\x82\xd3\xe4\x93\x02\x16\"\x11/v1/set_eye_color:\x01*\x12\x92\x01\n\nNavMapFeed\x12\x31.Anki.Vector.external_interface.NavMapFeedRequest\x1a\x32.Anki.Vector.external_interface.NavMapFeedResponse\"\x1b\x82\xd3\xe4\x93\x02\x15\"\x10/v1/nav_map_feed:\x01*0\x01\x62\x06proto3') + serialized_pb=_b('\n.anki_vector/messaging/external_interface.proto\x12\x1e\x41nki.Vector.external_interface\x1a\x1cgoogle/api/annotations.proto\x1a$anki_vector/messaging/behavior.proto\x1a anki_vector/messaging/cube.proto\x1a$anki_vector/messaging/messages.proto\x1a#anki_vector/messaging/nav_map.proto\x1a\"anki_vector/messaging/shared.proto*o\n\x0fProtocolVersion\x12\x1c\n\x18PROTOCOL_VERSION_UNKNOWN\x10\x00\x12\x1c\n\x18PROTOCOL_VERSION_MINIMUM\x10\x00\x12\x1c\n\x18PROTOCOL_VERSION_CURRENT\x10\x05\x1a\x02\x10\x01\x32\xacX\n\x11\x45xternalInterface\x12\xa3\x01\n\x0fProtocolVersion\x12\x36.Anki.Vector.external_interface.ProtocolVersionRequest\x1a\x37.Anki.Vector.external_interface.ProtocolVersionResponse\"\x1f\x82\xd3\xe4\x93\x02\x19\"\x14/v1/protocol_version:\x01*\x12\xab\x01\n\x11SDKInitialization\x12\x38.Anki.Vector.external_interface.SDKInitializationRequest\x1a\x39.Anki.Vector.external_interface.SDKInitializationResponse\"!\x82\xd3\xe4\x93\x02\x1b\"\x16/v1/sdk_initialization:\x01*\x12x\n\x0b\x44riveWheels\x12\x32.Anki.Vector.external_interface.DriveWheelsRequest\x1a\x33.Anki.Vector.external_interface.DriveWheelsResponse\"\x00\x12\x8c\x01\n\x14PlayAnimationTrigger\x12;.Anki.Vector.external_interface.PlayAnimationTriggerRequest\x1a\x35.Anki.Vector.external_interface.PlayAnimationResponse\"\x00\x12~\n\rPlayAnimation\x12\x34.Anki.Vector.external_interface.PlayAnimationRequest\x1a\x35.Anki.Vector.external_interface.PlayAnimationResponse\"\x00\x12\x9f\x01\n\x0eListAnimations\x12\x35.Anki.Vector.external_interface.ListAnimationsRequest\x1a\x36.Anki.Vector.external_interface.ListAnimationsResponse\"\x1e\x82\xd3\xe4\x93\x02\x18\"\x13/v1/list_animations:\x01*\x12\xbc\x01\n\x15ListAnimationTriggers\x12<.Anki.Vector.external_interface.ListAnimationTriggersRequest\x1a=.Anki.Vector.external_interface.ListAnimationTriggersResponse\"&\x82\xd3\xe4\x93\x02 \"\x1b/v1/list_animation_triggers:\x01*\x12o\n\x08MoveHead\x12/.Anki.Vector.external_interface.MoveHeadRequest\x1a\x30.Anki.Vector.external_interface.MoveHeadResponse\"\x00\x12o\n\x08MoveLift\x12/.Anki.Vector.external_interface.MoveLiftRequest\x1a\x30.Anki.Vector.external_interface.MoveLiftResponse\"\x00\x12~\n\rStopAllMotors\x12\x34.Anki.Vector.external_interface.StopAllMotorsRequest\x1a\x35.Anki.Vector.external_interface.StopAllMotorsResponse\"\x00\x12\xb5\x01\n\x13\x44isplayFaceImageRGB\x12:.Anki.Vector.external_interface.DisplayFaceImageRGBRequest\x1a;.Anki.Vector.external_interface.DisplayFaceImageRGBResponse\"%\x82\xd3\xe4\x93\x02\x1f\"\x1a/v1/display_face_image_rgb:\x01*\x12\x9d\x01\n\x0b\x45ventStream\x12,.Anki.Vector.external_interface.EventRequest\x1a-.Anki.Vector.external_interface.EventResponse\"/\x82\xd3\xe4\x93\x02)\"\x10/v1/event_stream:\x01*Z\x12\x12\x10/v1/event_stream0\x01\x12\x9c\x01\n\x1b\x45xternalAudioStreamPlayback\x12:.Anki.Vector.external_interface.ExternalAudioStreamRequest\x1a;.Anki.Vector.external_interface.ExternalAudioStreamResponse\"\x00(\x01\x30\x01\x12\x88\x01\n\x0f\x42\x65haviorControl\x12\x36.Anki.Vector.external_interface.BehaviorControlRequest\x1a\x37.Anki.Vector.external_interface.BehaviorControlResponse\"\x00(\x01\x30\x01\x12\xb8\x01\n\x14\x43\x61ncelFaceEnrollment\x12;.Anki.Vector.external_interface.CancelFaceEnrollmentRequest\x1a<.Anki.Vector.external_interface.CancelFaceEnrollmentResponse\"%\x82\xd3\xe4\x93\x02\x1f\"\x1a/v1/cancel_face_enrollment:\x01*\x12\xb8\x01\n\x14RequestEnrolledNames\x12;.Anki.Vector.external_interface.RequestEnrolledNamesRequest\x1a<.Anki.Vector.external_interface.RequestEnrolledNamesResponse\"%\x82\xd3\xe4\x93\x02\x1f\"\x1a/v1/request_enrolled_names:\x01*\x12\xc2\x01\n\x16UpdateEnrolledFaceByID\x12=.Anki.Vector.external_interface.UpdateEnrolledFaceByIDRequest\x1a>.Anki.Vector.external_interface.UpdateEnrolledFaceByIDResponse\")\x82\xd3\xe4\x93\x02#\"\x1e/v1/update_enrolled_face_by_id:\x01*\x12\xbe\x01\n\x15\x45raseEnrolledFaceByID\x12<.Anki.Vector.external_interface.EraseEnrolledFaceByIDRequest\x1a=.Anki.Vector.external_interface.EraseEnrolledFaceByIDResponse\"(\x82\xd3\xe4\x93\x02\"\"\x1d/v1/erase_enrolled_face_by_id:\x01*\x12\xbd\x01\n\x15\x45raseAllEnrolledFaces\x12<.Anki.Vector.external_interface.EraseAllEnrolledFacesRequest\x1a=.Anki.Vector.external_interface.EraseAllEnrolledFacesResponse\"\'\x82\xd3\xe4\x93\x02!\"\x1c/v1/erase_all_enrolled_faces:\x01*\x12\xa5\x01\n\x0fSetFaceToEnroll\x12\x36.Anki.Vector.external_interface.SetFaceToEnrollRequest\x1a\x37.Anki.Vector.external_interface.SetFaceToEnrollResponse\"!\x82\xd3\xe4\x93\x02\x1b\"\x16/v1/set_face_to_enroll:\x01*\x12\x8f\x01\n\nEnrollFace\x12\x31.Anki.Vector.external_interface.EnrollFaceRequest\x1a\x32.Anki.Vector.external_interface.EnrollFaceResponse\"\x1a\x82\xd3\xe4\x93\x02\x14\"\x0f/v1/enroll_face:\x01*\x12\xbc\x01\n\x15\x45nableMarkerDetection\x12<.Anki.Vector.external_interface.EnableMarkerDetectionRequest\x1a=.Anki.Vector.external_interface.EnableMarkerDetectionResponse\"&\x82\xd3\xe4\x93\x02 \"\x1b/v1/enable_marker_detection:\x01*\x12\xb4\x01\n\x13\x45nableFaceDetection\x12:.Anki.Vector.external_interface.EnableFaceDetectionRequest\x1a;.Anki.Vector.external_interface.EnableFaceDetectionResponse\"$\x82\xd3\xe4\x93\x02\x1e\"\x19/v1/enable_face_detection:\x01*\x12\xbc\x01\n\x15\x45nableMotionDetection\x12<.Anki.Vector.external_interface.EnableMotionDetectionRequest\x1a=.Anki.Vector.external_interface.EnableMotionDetectionResponse\"&\x82\xd3\xe4\x93\x02 \"\x1b/v1/enable_motion_detection:\x01*\x12\xa8\x01\n\x10\x45nableMirrorMode\x12\x37.Anki.Vector.external_interface.EnableMirrorModeRequest\x1a\x38.Anki.Vector.external_interface.EnableMirrorModeResponse\"!\x82\xd3\xe4\x93\x02\x1b\"\x16/v1/enable_mirror_mode:\x01*\x12\xb8\x01\n\x14\x45nableImageStreaming\x12;.Anki.Vector.external_interface.EnableImageStreamingRequest\x1a<.Anki.Vector.external_interface.EnableImageStreamingResponse\"%\x82\xd3\xe4\x93\x02\x1f\"\x1a/v1/enable_image_streaming:\x01*\x12\xc5\x01\n\x17IsImageStreamingEnabled\x12>.Anki.Vector.external_interface.IsImageStreamingEnabledRequest\x1a?.Anki.Vector.external_interface.IsImageStreamingEnabledResponse\")\x82\xd3\xe4\x93\x02#\"\x1e/v1/is_image_streaming_enabled:\x01*\x12\xb6\x01\n\x13\x43\x61ncelActionByIdTag\x12:.Anki.Vector.external_interface.CancelActionByIdTagRequest\x1a;.Anki.Vector.external_interface.CancelActionByIdTagResponse\"&\x82\xd3\xe4\x93\x02 \"\x1b/v1/cancel_action_by_id_tag:\x01*\x12\x9f\x01\n\x0e\x43\x61ncelBehavior\x12\x35.Anki.Vector.external_interface.CancelBehaviorRequest\x1a\x36.Anki.Vector.external_interface.CancelBehaviorResponse\"\x1e\x82\xd3\xe4\x93\x02\x18\"\x13/v1/cancel_behavior:\x01*\x12\x88\x01\n\x08GoToPose\x12/.Anki.Vector.external_interface.GoToPoseRequest\x1a\x30.Anki.Vector.external_interface.GoToPoseResponse\"\x19\x82\xd3\xe4\x93\x02\x13\"\x0e/v1/go_to_pose:\x01*\x12\x98\x01\n\x0c\x44ockWithCube\x12\x33.Anki.Vector.external_interface.DockWithCubeRequest\x1a\x34.Anki.Vector.external_interface.DockWithCubeResponse\"\x1d\x82\xd3\xe4\x93\x02\x17\"\x12/v1/dock_with_cube:\x01*\x12\xa4\x01\n\x0f\x44riveOffCharger\x12\x36.Anki.Vector.external_interface.DriveOffChargerRequest\x1a\x37.Anki.Vector.external_interface.DriveOffChargerResponse\" \x82\xd3\xe4\x93\x02\x1a\"\x15/v1/drive_off_charger:\x01*\x12\xa0\x01\n\x0e\x44riveOnCharger\x12\x35.Anki.Vector.external_interface.DriveOnChargerRequest\x1a\x36.Anki.Vector.external_interface.DriveOnChargerResponse\"\x1f\x82\xd3\xe4\x93\x02\x19\"\x14/v1/drive_on_charger:\x01*\x12\x8b\x01\n\tFindFaces\x12\x30.Anki.Vector.external_interface.FindFacesRequest\x1a\x31.Anki.Vector.external_interface.FindFacesResponse\"\x19\x82\xd3\xe4\x93\x02\x13\"\x0e/v1/find_faces:\x01*\x12\xad\x01\n\x11LookAroundInPlace\x12\x38.Anki.Vector.external_interface.LookAroundInPlaceRequest\x1a\x39.Anki.Vector.external_interface.LookAroundInPlaceResponse\"#\x82\xd3\xe4\x93\x02\x1d\"\x18/v1/look_around_in_place:\x01*\x12\x8b\x01\n\tRollBlock\x12\x30.Anki.Vector.external_interface.RollBlockRequest\x1a\x31.Anki.Vector.external_interface.RollBlockResponse\"\x19\x82\xd3\xe4\x93\x02\x13\"\x0e/v1/roll_block:\x01*\x12\x8f\x01\n\nPhotosInfo\x12\x31.Anki.Vector.external_interface.PhotosInfoRequest\x1a\x32.Anki.Vector.external_interface.PhotosInfoResponse\"\x1a\x82\xd3\xe4\x93\x02\x14\"\x0f/v1/photos_info:\x01*\x12z\n\x05Photo\x12,.Anki.Vector.external_interface.PhotoRequest\x1a-.Anki.Vector.external_interface.PhotoResponse\"\x14\x82\xd3\xe4\x93\x02\x0e\"\t/v1/photo:\x01*\x12\x8a\x01\n\tThumbnail\x12\x30.Anki.Vector.external_interface.ThumbnailRequest\x1a\x31.Anki.Vector.external_interface.ThumbnailResponse\"\x18\x82\xd3\xe4\x93\x02\x12\"\r/v1/thumbnail:\x01*\x12\x93\x01\n\x0b\x44\x65letePhoto\x12\x32.Anki.Vector.external_interface.DeletePhotoRequest\x1a\x33.Anki.Vector.external_interface.DeletePhotoResponse\"\x1b\x82\xd3\xe4\x93\x02\x15\"\x10/v1/delete_photo:\x01*\x12~\n\rDriveStraight\x12\x34.Anki.Vector.external_interface.DriveStraightRequest\x1a\x35.Anki.Vector.external_interface.DriveStraightResponse\"\x00\x12x\n\x0bTurnInPlace\x12\x32.Anki.Vector.external_interface.TurnInPlaceRequest\x1a\x33.Anki.Vector.external_interface.TurnInPlaceResponse\"\x00\x12{\n\x0cSetHeadAngle\x12\x33.Anki.Vector.external_interface.SetHeadAngleRequest\x1a\x34.Anki.Vector.external_interface.SetHeadAngleResponse\"\x00\x12~\n\rSetLiftHeight\x12\x34.Anki.Vector.external_interface.SetLiftHeightRequest\x1a\x35.Anki.Vector.external_interface.SetLiftHeightResponse\"\x00\x12\x84\x01\n\x0fTurnTowardsFace\x12\x36.Anki.Vector.external_interface.TurnTowardsFaceRequest\x1a\x37.Anki.Vector.external_interface.TurnTowardsFaceResponse\"\x00\x12u\n\nGoToObject\x12\x31.Anki.Vector.external_interface.GoToObjectRequest\x1a\x32.Anki.Vector.external_interface.GoToObjectResponse\"\x00\x12u\n\nRollObject\x12\x31.Anki.Vector.external_interface.RollObjectRequest\x1a\x32.Anki.Vector.external_interface.RollObjectResponse\"\x00\x12x\n\x0bPopAWheelie\x12\x32.Anki.Vector.external_interface.PopAWheelieRequest\x1a\x33.Anki.Vector.external_interface.PopAWheelieResponse\"\x00\x12{\n\x0cPickupObject\x12\x33.Anki.Vector.external_interface.PickupObjectRequest\x1a\x34.Anki.Vector.external_interface.PickupObjectResponse\"\x00\x12\x9c\x01\n\x17PlaceObjectOnGroundHere\x12>.Anki.Vector.external_interface.PlaceObjectOnGroundHereRequest\x1a?.Anki.Vector.external_interface.PlaceObjectOnGroundHereResponse\"\x00\x12~\n\x0fSetMasterVolume\x12\x33.Anki.Vector.external_interface.MasterVolumeRequest\x1a\x34.Anki.Vector.external_interface.MasterVolumeResponse\"\x00\x12\xaf\x01\n\x12UserAuthentication\x12\x39.Anki.Vector.external_interface.UserAuthenticationRequest\x1a:.Anki.Vector.external_interface.UserAuthenticationResponse\"\"\x82\xd3\xe4\x93\x02\x1c\"\x17/v1/user_authentication:\x01*\x12\x97\x01\n\x0c\x42\x61tteryState\x12\x33.Anki.Vector.external_interface.BatteryStateRequest\x1a\x34.Anki.Vector.external_interface.BatteryStateResponse\"\x1c\x82\xd3\xe4\x93\x02\x16\"\x11/v1/battery_state:\x01*\x12\x97\x01\n\x0cVersionState\x12\x33.Anki.Vector.external_interface.VersionStateRequest\x1a\x34.Anki.Vector.external_interface.VersionStateResponse\"\x1c\x82\xd3\xe4\x93\x02\x16\"\x11/v1/version_state:\x01*\x12\x83\x01\n\x07SayText\x12..Anki.Vector.external_interface.SayTextRequest\x1a/.Anki.Vector.external_interface.SayTextResponse\"\x17\x82\xd3\xe4\x93\x02\x11\"\x0c/v1/say_text:\x01*\x12\x93\x01\n\x0b\x43onnectCube\x12\x32.Anki.Vector.external_interface.ConnectCubeRequest\x1a\x33.Anki.Vector.external_interface.ConnectCubeResponse\"\x1b\x82\xd3\xe4\x93\x02\x15\"\x10/v1/connect_cube:\x01*\x12\x9f\x01\n\x0e\x44isconnectCube\x12\x35.Anki.Vector.external_interface.DisconnectCubeRequest\x1a\x36.Anki.Vector.external_interface.DisconnectCubeResponse\"\x1e\x82\xd3\xe4\x93\x02\x18\"\x13/v1/disconnect_cube:\x01*\x12\x9f\x01\n\x0e\x43ubesAvailable\x12\x35.Anki.Vector.external_interface.CubesAvailableRequest\x1a\x36.Anki.Vector.external_interface.CubesAvailableResponse\"\x1e\x82\xd3\xe4\x93\x02\x18\"\x13/v1/cubes_available:\x01*\x12\xa4\x01\n\x0f\x46lashCubeLights\x12\x36.Anki.Vector.external_interface.FlashCubeLightsRequest\x1a\x37.Anki.Vector.external_interface.FlashCubeLightsResponse\" \x82\xd3\xe4\x93\x02\x1a\"\x15/v1/flash_cube_lights:\x01*\x12\xb4\x01\n\x13\x46orgetPreferredCube\x12:.Anki.Vector.external_interface.ForgetPreferredCubeRequest\x1a;.Anki.Vector.external_interface.ForgetPreferredCubeResponse\"$\x82\xd3\xe4\x93\x02\x1e\"\x19/v1/forget_preferred_cube:\x01*\x12\xa8\x01\n\x10SetPreferredCube\x12\x37.Anki.Vector.external_interface.SetPreferredCubeRequest\x1a\x38.Anki.Vector.external_interface.SetPreferredCubeResponse\"!\x82\xd3\xe4\x93\x02\x1b\"\x16/v1/set_preferred_cube:\x01*\x12\xb4\x01\n\x13\x44\x65leteCustomObjects\x12:.Anki.Vector.external_interface.DeleteCustomObjectsRequest\x1a;.Anki.Vector.external_interface.DeleteCustomObjectsResponse\"$\x82\xd3\xe4\x93\x02\x1e\"\x19/v1/delete_custom_objects:\x01*\x12\xc5\x01\n\x17\x43reateFixedCustomObject\x12>.Anki.Vector.external_interface.CreateFixedCustomObjectRequest\x1a?.Anki.Vector.external_interface.CreateFixedCustomObjectResponse\")\x82\xd3\xe4\x93\x02#\"\x1e/v1/create_fixed_custom_object:\x01*\x12\xb0\x01\n\x12\x44\x65\x66ineCustomObject\x12\x39.Anki.Vector.external_interface.DefineCustomObjectRequest\x1a:.Anki.Vector.external_interface.DefineCustomObjectResponse\"#\x82\xd3\xe4\x93\x02\x1d\"\x18/v1/define_custom_object:\x01*\x12~\n\rSetCubeLights\x12\x34.Anki.Vector.external_interface.SetCubeLightsRequest\x1a\x35.Anki.Vector.external_interface.SetCubeLightsResponse\"\x00\x12\x8d\x01\n\tAudioFeed\x12\x30.Anki.Vector.external_interface.AudioFeedRequest\x1a\x31.Anki.Vector.external_interface.AudioFeedResponse\"\x19\x82\xd3\xe4\x93\x02\x13\"\x0e/v1/audio_feed:\x01*0\x01\x12\x91\x01\n\nCameraFeed\x12\x31.Anki.Vector.external_interface.CameraFeedRequest\x1a\x32.Anki.Vector.external_interface.CameraFeedResponse\"\x1a\x82\xd3\xe4\x93\x02\x14\"\x0f/v1/camera_feed:\x01*0\x01\x12\xb0\x01\n\x12\x43\x61ptureSingleImage\x12\x39.Anki.Vector.external_interface.CaptureSingleImageRequest\x1a:.Anki.Vector.external_interface.CaptureSingleImageResponse\"#\x82\xd3\xe4\x93\x02\x1d\"\x18/v1/capture_single_image:\x01*\x12\x9e\x01\n\x0fGetCameraConfig\x12\x33.Anki.Vector.external_interface.CameraConfigRequest\x1a\x34.Anki.Vector.external_interface.CameraConfigResponse\" \x82\xd3\xe4\x93\x02\x1a\"\x15/v1/get_camera_config:\x01*\x12\x94\x01\n\x0bSetEyeColor\x12\x32.Anki.Vector.external_interface.SetEyeColorRequest\x1a\x33.Anki.Vector.external_interface.SetEyeColorResponse\"\x1c\x82\xd3\xe4\x93\x02\x16\"\x11/v1/set_eye_color:\x01*\x12\x92\x01\n\nNavMapFeed\x12\x31.Anki.Vector.external_interface.NavMapFeedRequest\x1a\x32.Anki.Vector.external_interface.NavMapFeedResponse\"\x1b\x82\xd3\xe4\x93\x02\x15\"\x10/v1/nav_map_feed:\x01*0\x01\x12\xac\x01\n\x11SetCameraSettings\x12\x38.Anki.Vector.external_interface.SetCameraSettingsRequest\x1a\x39.Anki.Vector.external_interface.SetCameraSettingsResponse\"\"\x82\xd3\xe4\x93\x02\x1c\"\x17/v1/set_camera_settings:\x01*b\x06proto3') , dependencies=[google_dot_api_dot_annotations__pb2.DESCRIPTOR,anki__vector_dot_messaging_dot_behavior__pb2.DESCRIPTOR,anki__vector_dot_messaging_dot_cube__pb2.DESCRIPTOR,anki__vector_dot_messaging_dot_messages__pb2.DESCRIPTOR,anki__vector_dot_messaging_dot_nav__map__pb2.DESCRIPTOR,anki__vector_dot_messaging_dot_shared__pb2.DESCRIPTOR,]) @@ -76,7 +76,7 @@ index=0, options=None, serialized_start=409, - serialized_end=11254, + serialized_end=11717, methods=[ _descriptor.MethodDescriptor( name='ProtocolVersion', @@ -204,19 +204,10 @@ output_type=anki__vector_dot_messaging_dot_behavior__pb2._BEHAVIORCONTROLRESPONSE, options=None, ), - _descriptor.MethodDescriptor( - name='AssumeBehaviorControl', - full_name='Anki.Vector.external_interface.ExternalInterface.AssumeBehaviorControl', - index=14, - containing_service=None, - input_type=anki__vector_dot_messaging_dot_behavior__pb2._BEHAVIORCONTROLREQUEST, - output_type=anki__vector_dot_messaging_dot_behavior__pb2._BEHAVIORCONTROLRESPONSE, - options=_descriptor._ParseOptions(descriptor_pb2.MethodOptions(), _b('\202\323\344\223\002 \"\033/v1/assume_behavior_control:\001*')), - ), _descriptor.MethodDescriptor( name='CancelFaceEnrollment', full_name='Anki.Vector.external_interface.ExternalInterface.CancelFaceEnrollment', - index=15, + index=14, containing_service=None, input_type=anki__vector_dot_messaging_dot_messages__pb2._CANCELFACEENROLLMENTREQUEST, output_type=anki__vector_dot_messaging_dot_messages__pb2._CANCELFACEENROLLMENTRESPONSE, @@ -225,7 +216,7 @@ _descriptor.MethodDescriptor( name='RequestEnrolledNames', full_name='Anki.Vector.external_interface.ExternalInterface.RequestEnrolledNames', - index=16, + index=15, containing_service=None, input_type=anki__vector_dot_messaging_dot_messages__pb2._REQUESTENROLLEDNAMESREQUEST, output_type=anki__vector_dot_messaging_dot_messages__pb2._REQUESTENROLLEDNAMESRESPONSE, @@ -234,7 +225,7 @@ _descriptor.MethodDescriptor( name='UpdateEnrolledFaceByID', full_name='Anki.Vector.external_interface.ExternalInterface.UpdateEnrolledFaceByID', - index=17, + index=16, containing_service=None, input_type=anki__vector_dot_messaging_dot_messages__pb2._UPDATEENROLLEDFACEBYIDREQUEST, output_type=anki__vector_dot_messaging_dot_messages__pb2._UPDATEENROLLEDFACEBYIDRESPONSE, @@ -243,7 +234,7 @@ _descriptor.MethodDescriptor( name='EraseEnrolledFaceByID', full_name='Anki.Vector.external_interface.ExternalInterface.EraseEnrolledFaceByID', - index=18, + index=17, containing_service=None, input_type=anki__vector_dot_messaging_dot_messages__pb2._ERASEENROLLEDFACEBYIDREQUEST, output_type=anki__vector_dot_messaging_dot_messages__pb2._ERASEENROLLEDFACEBYIDRESPONSE, @@ -252,7 +243,7 @@ _descriptor.MethodDescriptor( name='EraseAllEnrolledFaces', full_name='Anki.Vector.external_interface.ExternalInterface.EraseAllEnrolledFaces', - index=19, + index=18, containing_service=None, input_type=anki__vector_dot_messaging_dot_messages__pb2._ERASEALLENROLLEDFACESREQUEST, output_type=anki__vector_dot_messaging_dot_messages__pb2._ERASEALLENROLLEDFACESRESPONSE, @@ -261,12 +252,21 @@ _descriptor.MethodDescriptor( name='SetFaceToEnroll', full_name='Anki.Vector.external_interface.ExternalInterface.SetFaceToEnroll', - index=20, + index=19, containing_service=None, input_type=anki__vector_dot_messaging_dot_messages__pb2._SETFACETOENROLLREQUEST, output_type=anki__vector_dot_messaging_dot_messages__pb2._SETFACETOENROLLRESPONSE, options=_descriptor._ParseOptions(descriptor_pb2.MethodOptions(), _b('\202\323\344\223\002\033\"\026/v1/set_face_to_enroll:\001*')), ), + _descriptor.MethodDescriptor( + name='EnrollFace', + full_name='Anki.Vector.external_interface.ExternalInterface.EnrollFace', + index=20, + containing_service=None, + input_type=anki__vector_dot_messaging_dot_messages__pb2._ENROLLFACEREQUEST, + output_type=anki__vector_dot_messaging_dot_messages__pb2._ENROLLFACERESPONSE, + options=_descriptor._ParseOptions(descriptor_pb2.MethodOptions(), _b('\202\323\344\223\002\024\"\017/v1/enroll_face:\001*')), + ), _descriptor.MethodDescriptor( name='EnableMarkerDetection', full_name='Anki.Vector.external_interface.ExternalInterface.EnableMarkerDetection', @@ -330,10 +330,19 @@ output_type=anki__vector_dot_messaging_dot_messages__pb2._CANCELACTIONBYIDTAGRESPONSE, options=_descriptor._ParseOptions(descriptor_pb2.MethodOptions(), _b('\202\323\344\223\002 \"\033/v1/cancel_action_by_id_tag:\001*')), ), + _descriptor.MethodDescriptor( + name='CancelBehavior', + full_name='Anki.Vector.external_interface.ExternalInterface.CancelBehavior', + index=28, + containing_service=None, + input_type=anki__vector_dot_messaging_dot_messages__pb2._CANCELBEHAVIORREQUEST, + output_type=anki__vector_dot_messaging_dot_messages__pb2._CANCELBEHAVIORRESPONSE, + options=_descriptor._ParseOptions(descriptor_pb2.MethodOptions(), _b('\202\323\344\223\002\030\"\023/v1/cancel_behavior:\001*')), + ), _descriptor.MethodDescriptor( name='GoToPose', full_name='Anki.Vector.external_interface.ExternalInterface.GoToPose', - index=28, + index=29, containing_service=None, input_type=anki__vector_dot_messaging_dot_messages__pb2._GOTOPOSEREQUEST, output_type=anki__vector_dot_messaging_dot_messages__pb2._GOTOPOSERESPONSE, @@ -342,7 +351,7 @@ _descriptor.MethodDescriptor( name='DockWithCube', full_name='Anki.Vector.external_interface.ExternalInterface.DockWithCube', - index=29, + index=30, containing_service=None, input_type=anki__vector_dot_messaging_dot_messages__pb2._DOCKWITHCUBEREQUEST, output_type=anki__vector_dot_messaging_dot_messages__pb2._DOCKWITHCUBERESPONSE, @@ -351,7 +360,7 @@ _descriptor.MethodDescriptor( name='DriveOffCharger', full_name='Anki.Vector.external_interface.ExternalInterface.DriveOffCharger', - index=30, + index=31, containing_service=None, input_type=anki__vector_dot_messaging_dot_messages__pb2._DRIVEOFFCHARGERREQUEST, output_type=anki__vector_dot_messaging_dot_messages__pb2._DRIVEOFFCHARGERRESPONSE, @@ -360,7 +369,7 @@ _descriptor.MethodDescriptor( name='DriveOnCharger', full_name='Anki.Vector.external_interface.ExternalInterface.DriveOnCharger', - index=31, + index=32, containing_service=None, input_type=anki__vector_dot_messaging_dot_messages__pb2._DRIVEONCHARGERREQUEST, output_type=anki__vector_dot_messaging_dot_messages__pb2._DRIVEONCHARGERRESPONSE, @@ -369,7 +378,7 @@ _descriptor.MethodDescriptor( name='FindFaces', full_name='Anki.Vector.external_interface.ExternalInterface.FindFaces', - index=32, + index=33, containing_service=None, input_type=anki__vector_dot_messaging_dot_messages__pb2._FINDFACESREQUEST, output_type=anki__vector_dot_messaging_dot_messages__pb2._FINDFACESRESPONSE, @@ -378,7 +387,7 @@ _descriptor.MethodDescriptor( name='LookAroundInPlace', full_name='Anki.Vector.external_interface.ExternalInterface.LookAroundInPlace', - index=33, + index=34, containing_service=None, input_type=anki__vector_dot_messaging_dot_messages__pb2._LOOKAROUNDINPLACEREQUEST, output_type=anki__vector_dot_messaging_dot_messages__pb2._LOOKAROUNDINPLACERESPONSE, @@ -387,7 +396,7 @@ _descriptor.MethodDescriptor( name='RollBlock', full_name='Anki.Vector.external_interface.ExternalInterface.RollBlock', - index=34, + index=35, containing_service=None, input_type=anki__vector_dot_messaging_dot_messages__pb2._ROLLBLOCKREQUEST, output_type=anki__vector_dot_messaging_dot_messages__pb2._ROLLBLOCKRESPONSE, @@ -396,7 +405,7 @@ _descriptor.MethodDescriptor( name='PhotosInfo', full_name='Anki.Vector.external_interface.ExternalInterface.PhotosInfo', - index=35, + index=36, containing_service=None, input_type=anki__vector_dot_messaging_dot_messages__pb2._PHOTOSINFOREQUEST, output_type=anki__vector_dot_messaging_dot_messages__pb2._PHOTOSINFORESPONSE, @@ -405,7 +414,7 @@ _descriptor.MethodDescriptor( name='Photo', full_name='Anki.Vector.external_interface.ExternalInterface.Photo', - index=36, + index=37, containing_service=None, input_type=anki__vector_dot_messaging_dot_messages__pb2._PHOTOREQUEST, output_type=anki__vector_dot_messaging_dot_messages__pb2._PHOTORESPONSE, @@ -414,7 +423,7 @@ _descriptor.MethodDescriptor( name='Thumbnail', full_name='Anki.Vector.external_interface.ExternalInterface.Thumbnail', - index=37, + index=38, containing_service=None, input_type=anki__vector_dot_messaging_dot_messages__pb2._THUMBNAILREQUEST, output_type=anki__vector_dot_messaging_dot_messages__pb2._THUMBNAILRESPONSE, @@ -423,7 +432,7 @@ _descriptor.MethodDescriptor( name='DeletePhoto', full_name='Anki.Vector.external_interface.ExternalInterface.DeletePhoto', - index=38, + index=39, containing_service=None, input_type=anki__vector_dot_messaging_dot_messages__pb2._DELETEPHOTOREQUEST, output_type=anki__vector_dot_messaging_dot_messages__pb2._DELETEPHOTORESPONSE, @@ -432,7 +441,7 @@ _descriptor.MethodDescriptor( name='DriveStraight', full_name='Anki.Vector.external_interface.ExternalInterface.DriveStraight', - index=39, + index=40, containing_service=None, input_type=anki__vector_dot_messaging_dot_messages__pb2._DRIVESTRAIGHTREQUEST, output_type=anki__vector_dot_messaging_dot_messages__pb2._DRIVESTRAIGHTRESPONSE, @@ -441,7 +450,7 @@ _descriptor.MethodDescriptor( name='TurnInPlace', full_name='Anki.Vector.external_interface.ExternalInterface.TurnInPlace', - index=40, + index=41, containing_service=None, input_type=anki__vector_dot_messaging_dot_messages__pb2._TURNINPLACEREQUEST, output_type=anki__vector_dot_messaging_dot_messages__pb2._TURNINPLACERESPONSE, @@ -450,7 +459,7 @@ _descriptor.MethodDescriptor( name='SetHeadAngle', full_name='Anki.Vector.external_interface.ExternalInterface.SetHeadAngle', - index=41, + index=42, containing_service=None, input_type=anki__vector_dot_messaging_dot_messages__pb2._SETHEADANGLEREQUEST, output_type=anki__vector_dot_messaging_dot_messages__pb2._SETHEADANGLERESPONSE, @@ -459,7 +468,7 @@ _descriptor.MethodDescriptor( name='SetLiftHeight', full_name='Anki.Vector.external_interface.ExternalInterface.SetLiftHeight', - index=42, + index=43, containing_service=None, input_type=anki__vector_dot_messaging_dot_messages__pb2._SETLIFTHEIGHTREQUEST, output_type=anki__vector_dot_messaging_dot_messages__pb2._SETLIFTHEIGHTRESPONSE, @@ -468,7 +477,7 @@ _descriptor.MethodDescriptor( name='TurnTowardsFace', full_name='Anki.Vector.external_interface.ExternalInterface.TurnTowardsFace', - index=43, + index=44, containing_service=None, input_type=anki__vector_dot_messaging_dot_messages__pb2._TURNTOWARDSFACEREQUEST, output_type=anki__vector_dot_messaging_dot_messages__pb2._TURNTOWARDSFACERESPONSE, @@ -477,7 +486,7 @@ _descriptor.MethodDescriptor( name='GoToObject', full_name='Anki.Vector.external_interface.ExternalInterface.GoToObject', - index=44, + index=45, containing_service=None, input_type=anki__vector_dot_messaging_dot_messages__pb2._GOTOOBJECTREQUEST, output_type=anki__vector_dot_messaging_dot_messages__pb2._GOTOOBJECTRESPONSE, @@ -486,7 +495,7 @@ _descriptor.MethodDescriptor( name='RollObject', full_name='Anki.Vector.external_interface.ExternalInterface.RollObject', - index=45, + index=46, containing_service=None, input_type=anki__vector_dot_messaging_dot_messages__pb2._ROLLOBJECTREQUEST, output_type=anki__vector_dot_messaging_dot_messages__pb2._ROLLOBJECTRESPONSE, @@ -495,7 +504,7 @@ _descriptor.MethodDescriptor( name='PopAWheelie', full_name='Anki.Vector.external_interface.ExternalInterface.PopAWheelie', - index=46, + index=47, containing_service=None, input_type=anki__vector_dot_messaging_dot_messages__pb2._POPAWHEELIEREQUEST, output_type=anki__vector_dot_messaging_dot_messages__pb2._POPAWHEELIERESPONSE, @@ -504,7 +513,7 @@ _descriptor.MethodDescriptor( name='PickupObject', full_name='Anki.Vector.external_interface.ExternalInterface.PickupObject', - index=47, + index=48, containing_service=None, input_type=anki__vector_dot_messaging_dot_messages__pb2._PICKUPOBJECTREQUEST, output_type=anki__vector_dot_messaging_dot_messages__pb2._PICKUPOBJECTRESPONSE, @@ -513,7 +522,7 @@ _descriptor.MethodDescriptor( name='PlaceObjectOnGroundHere', full_name='Anki.Vector.external_interface.ExternalInterface.PlaceObjectOnGroundHere', - index=48, + index=49, containing_service=None, input_type=anki__vector_dot_messaging_dot_messages__pb2._PLACEOBJECTONGROUNDHEREREQUEST, output_type=anki__vector_dot_messaging_dot_messages__pb2._PLACEOBJECTONGROUNDHERERESPONSE, @@ -522,7 +531,7 @@ _descriptor.MethodDescriptor( name='SetMasterVolume', full_name='Anki.Vector.external_interface.ExternalInterface.SetMasterVolume', - index=49, + index=50, containing_service=None, input_type=anki__vector_dot_messaging_dot_messages__pb2._MASTERVOLUMEREQUEST, output_type=anki__vector_dot_messaging_dot_messages__pb2._MASTERVOLUMERESPONSE, @@ -531,7 +540,7 @@ _descriptor.MethodDescriptor( name='UserAuthentication', full_name='Anki.Vector.external_interface.ExternalInterface.UserAuthentication', - index=50, + index=51, containing_service=None, input_type=anki__vector_dot_messaging_dot_shared__pb2._USERAUTHENTICATIONREQUEST, output_type=anki__vector_dot_messaging_dot_shared__pb2._USERAUTHENTICATIONRESPONSE, @@ -540,7 +549,7 @@ _descriptor.MethodDescriptor( name='BatteryState', full_name='Anki.Vector.external_interface.ExternalInterface.BatteryState', - index=51, + index=52, containing_service=None, input_type=anki__vector_dot_messaging_dot_messages__pb2._BATTERYSTATEREQUEST, output_type=anki__vector_dot_messaging_dot_messages__pb2._BATTERYSTATERESPONSE, @@ -549,7 +558,7 @@ _descriptor.MethodDescriptor( name='VersionState', full_name='Anki.Vector.external_interface.ExternalInterface.VersionState', - index=52, + index=53, containing_service=None, input_type=anki__vector_dot_messaging_dot_messages__pb2._VERSIONSTATEREQUEST, output_type=anki__vector_dot_messaging_dot_messages__pb2._VERSIONSTATERESPONSE, @@ -558,7 +567,7 @@ _descriptor.MethodDescriptor( name='SayText', full_name='Anki.Vector.external_interface.ExternalInterface.SayText', - index=53, + index=54, containing_service=None, input_type=anki__vector_dot_messaging_dot_messages__pb2._SAYTEXTREQUEST, output_type=anki__vector_dot_messaging_dot_messages__pb2._SAYTEXTRESPONSE, @@ -567,7 +576,7 @@ _descriptor.MethodDescriptor( name='ConnectCube', full_name='Anki.Vector.external_interface.ExternalInterface.ConnectCube', - index=54, + index=55, containing_service=None, input_type=anki__vector_dot_messaging_dot_cube__pb2._CONNECTCUBEREQUEST, output_type=anki__vector_dot_messaging_dot_cube__pb2._CONNECTCUBERESPONSE, @@ -576,7 +585,7 @@ _descriptor.MethodDescriptor( name='DisconnectCube', full_name='Anki.Vector.external_interface.ExternalInterface.DisconnectCube', - index=55, + index=56, containing_service=None, input_type=anki__vector_dot_messaging_dot_cube__pb2._DISCONNECTCUBEREQUEST, output_type=anki__vector_dot_messaging_dot_cube__pb2._DISCONNECTCUBERESPONSE, @@ -585,7 +594,7 @@ _descriptor.MethodDescriptor( name='CubesAvailable', full_name='Anki.Vector.external_interface.ExternalInterface.CubesAvailable', - index=56, + index=57, containing_service=None, input_type=anki__vector_dot_messaging_dot_cube__pb2._CUBESAVAILABLEREQUEST, output_type=anki__vector_dot_messaging_dot_cube__pb2._CUBESAVAILABLERESPONSE, @@ -594,7 +603,7 @@ _descriptor.MethodDescriptor( name='FlashCubeLights', full_name='Anki.Vector.external_interface.ExternalInterface.FlashCubeLights', - index=57, + index=58, containing_service=None, input_type=anki__vector_dot_messaging_dot_cube__pb2._FLASHCUBELIGHTSREQUEST, output_type=anki__vector_dot_messaging_dot_cube__pb2._FLASHCUBELIGHTSRESPONSE, @@ -603,7 +612,7 @@ _descriptor.MethodDescriptor( name='ForgetPreferredCube', full_name='Anki.Vector.external_interface.ExternalInterface.ForgetPreferredCube', - index=58, + index=59, containing_service=None, input_type=anki__vector_dot_messaging_dot_cube__pb2._FORGETPREFERREDCUBEREQUEST, output_type=anki__vector_dot_messaging_dot_cube__pb2._FORGETPREFERREDCUBERESPONSE, @@ -612,7 +621,7 @@ _descriptor.MethodDescriptor( name='SetPreferredCube', full_name='Anki.Vector.external_interface.ExternalInterface.SetPreferredCube', - index=59, + index=60, containing_service=None, input_type=anki__vector_dot_messaging_dot_cube__pb2._SETPREFERREDCUBEREQUEST, output_type=anki__vector_dot_messaging_dot_cube__pb2._SETPREFERREDCUBERESPONSE, @@ -621,7 +630,7 @@ _descriptor.MethodDescriptor( name='DeleteCustomObjects', full_name='Anki.Vector.external_interface.ExternalInterface.DeleteCustomObjects', - index=60, + index=61, containing_service=None, input_type=anki__vector_dot_messaging_dot_cube__pb2._DELETECUSTOMOBJECTSREQUEST, output_type=anki__vector_dot_messaging_dot_cube__pb2._DELETECUSTOMOBJECTSRESPONSE, @@ -630,7 +639,7 @@ _descriptor.MethodDescriptor( name='CreateFixedCustomObject', full_name='Anki.Vector.external_interface.ExternalInterface.CreateFixedCustomObject', - index=61, + index=62, containing_service=None, input_type=anki__vector_dot_messaging_dot_cube__pb2._CREATEFIXEDCUSTOMOBJECTREQUEST, output_type=anki__vector_dot_messaging_dot_cube__pb2._CREATEFIXEDCUSTOMOBJECTRESPONSE, @@ -639,7 +648,7 @@ _descriptor.MethodDescriptor( name='DefineCustomObject', full_name='Anki.Vector.external_interface.ExternalInterface.DefineCustomObject', - index=62, + index=63, containing_service=None, input_type=anki__vector_dot_messaging_dot_cube__pb2._DEFINECUSTOMOBJECTREQUEST, output_type=anki__vector_dot_messaging_dot_cube__pb2._DEFINECUSTOMOBJECTRESPONSE, @@ -648,7 +657,7 @@ _descriptor.MethodDescriptor( name='SetCubeLights', full_name='Anki.Vector.external_interface.ExternalInterface.SetCubeLights', - index=63, + index=64, containing_service=None, input_type=anki__vector_dot_messaging_dot_cube__pb2._SETCUBELIGHTSREQUEST, output_type=anki__vector_dot_messaging_dot_cube__pb2._SETCUBELIGHTSRESPONSE, @@ -657,7 +666,7 @@ _descriptor.MethodDescriptor( name='AudioFeed', full_name='Anki.Vector.external_interface.ExternalInterface.AudioFeed', - index=64, + index=65, containing_service=None, input_type=anki__vector_dot_messaging_dot_messages__pb2._AUDIOFEEDREQUEST, output_type=anki__vector_dot_messaging_dot_messages__pb2._AUDIOFEEDRESPONSE, @@ -666,7 +675,7 @@ _descriptor.MethodDescriptor( name='CameraFeed', full_name='Anki.Vector.external_interface.ExternalInterface.CameraFeed', - index=65, + index=66, containing_service=None, input_type=anki__vector_dot_messaging_dot_messages__pb2._CAMERAFEEDREQUEST, output_type=anki__vector_dot_messaging_dot_messages__pb2._CAMERAFEEDRESPONSE, @@ -675,16 +684,25 @@ _descriptor.MethodDescriptor( name='CaptureSingleImage', full_name='Anki.Vector.external_interface.ExternalInterface.CaptureSingleImage', - index=66, + index=67, containing_service=None, input_type=anki__vector_dot_messaging_dot_messages__pb2._CAPTURESINGLEIMAGEREQUEST, output_type=anki__vector_dot_messaging_dot_messages__pb2._CAPTURESINGLEIMAGERESPONSE, options=_descriptor._ParseOptions(descriptor_pb2.MethodOptions(), _b('\202\323\344\223\002\035\"\030/v1/capture_single_image:\001*')), ), + _descriptor.MethodDescriptor( + name='GetCameraConfig', + full_name='Anki.Vector.external_interface.ExternalInterface.GetCameraConfig', + index=68, + containing_service=None, + input_type=anki__vector_dot_messaging_dot_messages__pb2._CAMERACONFIGREQUEST, + output_type=anki__vector_dot_messaging_dot_messages__pb2._CAMERACONFIGRESPONSE, + options=_descriptor._ParseOptions(descriptor_pb2.MethodOptions(), _b('\202\323\344\223\002\032\"\025/v1/get_camera_config:\001*')), + ), _descriptor.MethodDescriptor( name='SetEyeColor', full_name='Anki.Vector.external_interface.ExternalInterface.SetEyeColor', - index=67, + index=69, containing_service=None, input_type=anki__vector_dot_messaging_dot_messages__pb2._SETEYECOLORREQUEST, output_type=anki__vector_dot_messaging_dot_messages__pb2._SETEYECOLORRESPONSE, @@ -693,12 +711,21 @@ _descriptor.MethodDescriptor( name='NavMapFeed', full_name='Anki.Vector.external_interface.ExternalInterface.NavMapFeed', - index=68, + index=70, containing_service=None, input_type=anki__vector_dot_messaging_dot_nav__map__pb2._NAVMAPFEEDREQUEST, output_type=anki__vector_dot_messaging_dot_nav__map__pb2._NAVMAPFEEDRESPONSE, options=_descriptor._ParseOptions(descriptor_pb2.MethodOptions(), _b('\202\323\344\223\002\025\"\020/v1/nav_map_feed:\001*')), ), + _descriptor.MethodDescriptor( + name='SetCameraSettings', + full_name='Anki.Vector.external_interface.ExternalInterface.SetCameraSettings', + index=71, + containing_service=None, + input_type=anki__vector_dot_messaging_dot_messages__pb2._SETCAMERASETTINGSREQUEST, + output_type=anki__vector_dot_messaging_dot_messages__pb2._SETCAMERASETTINGSRESPONSE, + options=_descriptor._ParseOptions(descriptor_pb2.MethodOptions(), _b('\202\323\344\223\002\034\"\027/v1/set_camera_settings:\001*')), + ), ]) _sym_db.RegisterServiceDescriptor(_EXTERNALINTERFACE) diff --git a/anki_vector/messaging/external_interface_pb2_grpc.py b/anki_vector/messaging/external_interface_pb2_grpc.py index a41ae3e..ee065da 100644 --- a/anki_vector/messaging/external_interface_pb2_grpc.py +++ b/anki_vector/messaging/external_interface_pb2_grpc.py @@ -88,11 +88,6 @@ def __init__(self, channel): request_serializer=anki__vector_dot_messaging_dot_behavior__pb2.BehaviorControlRequest.SerializeToString, response_deserializer=anki__vector_dot_messaging_dot_behavior__pb2.BehaviorControlResponse.FromString, ) - self.AssumeBehaviorControl = channel.unary_stream( - '/Anki.Vector.external_interface.ExternalInterface/AssumeBehaviorControl', - request_serializer=anki__vector_dot_messaging_dot_behavior__pb2.BehaviorControlRequest.SerializeToString, - response_deserializer=anki__vector_dot_messaging_dot_behavior__pb2.BehaviorControlResponse.FromString, - ) self.CancelFaceEnrollment = channel.unary_unary( '/Anki.Vector.external_interface.ExternalInterface/CancelFaceEnrollment', request_serializer=anki__vector_dot_messaging_dot_messages__pb2.CancelFaceEnrollmentRequest.SerializeToString, @@ -123,6 +118,11 @@ def __init__(self, channel): request_serializer=anki__vector_dot_messaging_dot_messages__pb2.SetFaceToEnrollRequest.SerializeToString, response_deserializer=anki__vector_dot_messaging_dot_messages__pb2.SetFaceToEnrollResponse.FromString, ) + self.EnrollFace = channel.unary_unary( + '/Anki.Vector.external_interface.ExternalInterface/EnrollFace', + request_serializer=anki__vector_dot_messaging_dot_messages__pb2.EnrollFaceRequest.SerializeToString, + response_deserializer=anki__vector_dot_messaging_dot_messages__pb2.EnrollFaceResponse.FromString, + ) self.EnableMarkerDetection = channel.unary_unary( '/Anki.Vector.external_interface.ExternalInterface/EnableMarkerDetection', request_serializer=anki__vector_dot_messaging_dot_messages__pb2.EnableMarkerDetectionRequest.SerializeToString, @@ -158,6 +158,11 @@ def __init__(self, channel): request_serializer=anki__vector_dot_messaging_dot_messages__pb2.CancelActionByIdTagRequest.SerializeToString, response_deserializer=anki__vector_dot_messaging_dot_messages__pb2.CancelActionByIdTagResponse.FromString, ) + self.CancelBehavior = channel.unary_unary( + '/Anki.Vector.external_interface.ExternalInterface/CancelBehavior', + request_serializer=anki__vector_dot_messaging_dot_messages__pb2.CancelBehaviorRequest.SerializeToString, + response_deserializer=anki__vector_dot_messaging_dot_messages__pb2.CancelBehaviorResponse.FromString, + ) self.GoToPose = channel.unary_unary( '/Anki.Vector.external_interface.ExternalInterface/GoToPose', request_serializer=anki__vector_dot_messaging_dot_messages__pb2.GoToPoseRequest.SerializeToString, @@ -353,6 +358,11 @@ def __init__(self, channel): request_serializer=anki__vector_dot_messaging_dot_messages__pb2.CaptureSingleImageRequest.SerializeToString, response_deserializer=anki__vector_dot_messaging_dot_messages__pb2.CaptureSingleImageResponse.FromString, ) + self.GetCameraConfig = channel.unary_unary( + '/Anki.Vector.external_interface.ExternalInterface/GetCameraConfig', + request_serializer=anki__vector_dot_messaging_dot_messages__pb2.CameraConfigRequest.SerializeToString, + response_deserializer=anki__vector_dot_messaging_dot_messages__pb2.CameraConfigResponse.FromString, + ) self.SetEyeColor = channel.unary_unary( '/Anki.Vector.external_interface.ExternalInterface/SetEyeColor', request_serializer=anki__vector_dot_messaging_dot_messages__pb2.SetEyeColorRequest.SerializeToString, @@ -363,6 +373,11 @@ def __init__(self, channel): request_serializer=anki__vector_dot_messaging_dot_nav__map__pb2.NavMapFeedRequest.SerializeToString, response_deserializer=anki__vector_dot_messaging_dot_nav__map__pb2.NavMapFeedResponse.FromString, ) + self.SetCameraSettings = channel.unary_unary( + '/Anki.Vector.external_interface.ExternalInterface/SetCameraSettings', + request_serializer=anki__vector_dot_messaging_dot_messages__pb2.SetCameraSettingsRequest.SerializeToString, + response_deserializer=anki__vector_dot_messaging_dot_messages__pb2.SetCameraSettingsResponse.FromString, + ) class ExternalInterfaceServicer(object): @@ -468,13 +483,6 @@ def BehaviorControl(self, request_iterator, context): context.set_details('Method not implemented!') raise NotImplementedError('Method not implemented!') - def AssumeBehaviorControl(self, request, context): - """Acquire control of Vector's AI system. - """ - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - def CancelFaceEnrollment(self, request, context): # missing associated documentation comment in .proto file pass @@ -517,6 +525,13 @@ def SetFaceToEnroll(self, request, context): context.set_details('Method not implemented!') raise NotImplementedError('Method not implemented!') + def EnrollFace(self, request, context): + """Enroll a face. Must be used with SetFaceToEnroll + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + def EnableMarkerDetection(self, request, context): # missing associated documentation comment in .proto file pass @@ -566,6 +581,13 @@ def CancelActionByIdTag(self, request, context): context.set_details('Method not implemented!') raise NotImplementedError('Method not implemented!') + def CancelBehavior(self, request, context): + """Cancel running SDK Behavior + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + def GoToPose(self, request, context): """Tells Vector to drive to the specified pose and orientation. """ @@ -848,6 +870,13 @@ def CaptureSingleImage(self, request, context): context.set_details('Method not implemented!') raise NotImplementedError('Method not implemented!') + def GetCameraConfig(self, request, context): + """Get Vector's camera configuration. + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + def SetEyeColor(self, request, context): """Set Vector's eye color. """ @@ -862,6 +891,13 @@ def NavMapFeed(self, request, context): context.set_details('Method not implemented!') raise NotImplementedError('Method not implemented!') + def SetCameraSettings(self, request, context): + """Set Vector's camera settings + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + def add_ExternalInterfaceServicer_to_server(servicer, server): rpc_method_handlers = { @@ -935,11 +971,6 @@ def add_ExternalInterfaceServicer_to_server(servicer, server): request_deserializer=anki__vector_dot_messaging_dot_behavior__pb2.BehaviorControlRequest.FromString, response_serializer=anki__vector_dot_messaging_dot_behavior__pb2.BehaviorControlResponse.SerializeToString, ), - 'AssumeBehaviorControl': grpc.unary_stream_rpc_method_handler( - servicer.AssumeBehaviorControl, - request_deserializer=anki__vector_dot_messaging_dot_behavior__pb2.BehaviorControlRequest.FromString, - response_serializer=anki__vector_dot_messaging_dot_behavior__pb2.BehaviorControlResponse.SerializeToString, - ), 'CancelFaceEnrollment': grpc.unary_unary_rpc_method_handler( servicer.CancelFaceEnrollment, request_deserializer=anki__vector_dot_messaging_dot_messages__pb2.CancelFaceEnrollmentRequest.FromString, @@ -970,6 +1001,11 @@ def add_ExternalInterfaceServicer_to_server(servicer, server): request_deserializer=anki__vector_dot_messaging_dot_messages__pb2.SetFaceToEnrollRequest.FromString, response_serializer=anki__vector_dot_messaging_dot_messages__pb2.SetFaceToEnrollResponse.SerializeToString, ), + 'EnrollFace': grpc.unary_unary_rpc_method_handler( + servicer.EnrollFace, + request_deserializer=anki__vector_dot_messaging_dot_messages__pb2.EnrollFaceRequest.FromString, + response_serializer=anki__vector_dot_messaging_dot_messages__pb2.EnrollFaceResponse.SerializeToString, + ), 'EnableMarkerDetection': grpc.unary_unary_rpc_method_handler( servicer.EnableMarkerDetection, request_deserializer=anki__vector_dot_messaging_dot_messages__pb2.EnableMarkerDetectionRequest.FromString, @@ -1005,6 +1041,11 @@ def add_ExternalInterfaceServicer_to_server(servicer, server): request_deserializer=anki__vector_dot_messaging_dot_messages__pb2.CancelActionByIdTagRequest.FromString, response_serializer=anki__vector_dot_messaging_dot_messages__pb2.CancelActionByIdTagResponse.SerializeToString, ), + 'CancelBehavior': grpc.unary_unary_rpc_method_handler( + servicer.CancelBehavior, + request_deserializer=anki__vector_dot_messaging_dot_messages__pb2.CancelBehaviorRequest.FromString, + response_serializer=anki__vector_dot_messaging_dot_messages__pb2.CancelBehaviorResponse.SerializeToString, + ), 'GoToPose': grpc.unary_unary_rpc_method_handler( servicer.GoToPose, request_deserializer=anki__vector_dot_messaging_dot_messages__pb2.GoToPoseRequest.FromString, @@ -1200,6 +1241,11 @@ def add_ExternalInterfaceServicer_to_server(servicer, server): request_deserializer=anki__vector_dot_messaging_dot_messages__pb2.CaptureSingleImageRequest.FromString, response_serializer=anki__vector_dot_messaging_dot_messages__pb2.CaptureSingleImageResponse.SerializeToString, ), + 'GetCameraConfig': grpc.unary_unary_rpc_method_handler( + servicer.GetCameraConfig, + request_deserializer=anki__vector_dot_messaging_dot_messages__pb2.CameraConfigRequest.FromString, + response_serializer=anki__vector_dot_messaging_dot_messages__pb2.CameraConfigResponse.SerializeToString, + ), 'SetEyeColor': grpc.unary_unary_rpc_method_handler( servicer.SetEyeColor, request_deserializer=anki__vector_dot_messaging_dot_messages__pb2.SetEyeColorRequest.FromString, @@ -1210,6 +1256,11 @@ def add_ExternalInterfaceServicer_to_server(servicer, server): request_deserializer=anki__vector_dot_messaging_dot_nav__map__pb2.NavMapFeedRequest.FromString, response_serializer=anki__vector_dot_messaging_dot_nav__map__pb2.NavMapFeedResponse.SerializeToString, ), + 'SetCameraSettings': grpc.unary_unary_rpc_method_handler( + servicer.SetCameraSettings, + request_deserializer=anki__vector_dot_messaging_dot_messages__pb2.SetCameraSettingsRequest.FromString, + response_serializer=anki__vector_dot_messaging_dot_messages__pb2.SetCameraSettingsResponse.SerializeToString, + ), } generic_handler = grpc.method_handlers_generic_handler( 'Anki.Vector.external_interface.ExternalInterface', rpc_method_handlers) diff --git a/anki_vector/messaging/messages.proto b/anki_vector/messaging/messages.proto index 37bb0d8..9955572 100644 --- a/anki_vector/messaging/messages.proto +++ b/anki_vector/messaging/messages.proto @@ -78,7 +78,7 @@ message StopAllMotorsRequest { } // See the StopAllMotors rpc for more details. -message StopAllMotorsResponse { +message StopAllMotorsResponse { ResponseStatus status = 1; } @@ -170,6 +170,7 @@ message MeetVictorFaceScanComplete message Status { oneof status_type { + FeatureStatus feature_status = 1; MeetVictorFaceScanStarted meet_victor_face_scan_started = 2; MeetVictorFaceScanComplete meet_victor_face_scan_complete = 3; FaceEnrollmentCompleted face_enrollment_completed = 4; @@ -193,7 +194,7 @@ message PoseStruct { float x = 1; float y = 2; float z = 3; - + // Rotation quaternion float q0 = 4; float q1 = 5; @@ -250,8 +251,7 @@ message RobotState { TouchData touch_data = 17; } -enum RobotStatus -{ +enum RobotStatus { ROBOT_STATUS_NONE = 0x0; ROBOT_STATUS_IS_MOVING = 0x1; ROBOT_STATUS_IS_CARRYING_BLOCK = 0x2; @@ -286,10 +286,78 @@ message CladRect float height = 4; } +// (v1.7) +message RobotObservedMotion { + uint32 timestamp = 1; // Timestamp of the corresponding image + + // Area of the supporting region for the point, as a fraction of the image + float img_area = 2; + int32 img_x = 3; // Pixel coordinate of the point in the image, relative to top-left corner. + int32 img_y = 4; // Pixel coordinate of the point in the image, relative to top-left corner. + + // Area of the supporting region for the point, as a fraction of the ground ROI + // If unable to map to the ground, area=0 + float ground_area = 5; + int32 ground_x = 6; // Coordinates of the point on the ground, relative to robot, in mm + int32 ground_y = 7; // Coordinates of the point on the ground, relative to robot, in mm + + // Top area + // Area of the supporting region for the point, as a fraction of the top region + float top_img_area = 8; + int32 top_img_x = 9; // Pixel coordinate of the point in the image, relative to top-left corner. + int32 top_img_y = 10; // Pixel coordinate of the point in the image, relative to top-left corner. + + // Bottom area + // Area of the supporting region for the point, as a fraction of the bottom region + float bottom_img_area = 11; + int32 bottom_img_x = 12; // Pixel coordinate of the point in the image, relative to top-left corner. + int32 bottom_img_y = 13; // Pixel coordinate of the point in the image, relative to top-left corner. + + // Left area + // Area of the supporting region for the point, as a fraction of the left region + float left_img_area = 14; + int32 left_img_x = 15; // Pixel coordinate of the point in the image, relative to top-left corner. + int32 left_img_y = 16; // Pixel coordinate of the point in the image, relative to top-left corner. + + // Right area + // Area of the supporting region for the point, as a fraction of the right region + float right_img_area = 17; + int32 right_img_x = 18; // Pixel coordinate of the point in the image, relative to top-left corner. + int32 right_img_y = 19; // Pixel coordinate of the point in the image, relative to top-left corner. +} + + // Event confirming that an enrolled face has been removed from the robot. +message RobotErasedEnrolledFace { + int32 face_id = 1; + string name = 2; +} + +enum UnexpectedMovementType { + TURNED_BUT_STOPPED = 0; + TURNED_IN_SAME_DIRECTION = 1; + TURNED_IN_OPPOSITE_DIRECTION = 2; + ROTATING_WITHOUT_MOTORS = 3; +} + +enum UnexpectedMovementSide { + UNKNOWN = 0; + FRONT = 1; + BACK = 2; + LEFT = 3; + RIGHT = 4; +} + +// Event dispatched when the robot does not move as expected. Has additional information +// such as the direction and type of unexpected motion. +message UnexpectedMovement { + uint32 timestamp = 1; + UnexpectedMovementType movement_type = 2; + UnexpectedMovementSide movement_side = 3; +} + // This is an int8 on the clad side. // Proto field names are prefixed with "EXPRESSION_" -enum FacialExpression -{ +enum FacialExpression { option allow_alias = true; EXPRESSION_UNKNOWN = 0; // e.g. expression estimation disabled. @@ -330,8 +398,7 @@ message RobotChangedObservedFaceID { int32 new_id = 2; } -enum FaceEnrollmentResult -{ +enum FaceEnrollmentResult { SUCCESS = 0; // Failures: @@ -373,7 +440,7 @@ message LoadedKnownFace { message RobotRenamedEnrolledFace { int32 face_id = 1; - string name = 2; + string name = 2; } message RequestEnrolledNamesResponse { @@ -421,8 +488,17 @@ message SetFaceToEnrollResponse { ResponseStatus status = 1; } -enum BehaviorResults -{ +// (v1.7) +message EnrollFaceRequest { +} + +// (v1.7) +message EnrollFaceResponse { + ResponseStatus status = 1; + BehaviorResults result = 2; +} + +enum BehaviorResults { BEHAVIOR_INVALID_STATE = 0; BEHAVIOR_COMPLETE_STATE = 1; BEHAVIOR_WONT_ACTIVATE_STATE = 2; @@ -540,7 +616,7 @@ message PhotoTaken { uint32 photo_id = 1; } -// Struct containing all the information relevant to how a +// Struct containing all the information relevant to how a // path should be modified or traversed. message PathMotionProfile { float speed_mmps = 1; @@ -556,6 +632,7 @@ message PathMotionProfile { bool is_custom = 11; } +// These codes are duplicated from actionResults.clad file and need to be kept in sync // The possible results of running an action. message ActionResult { // The possible results of running an action. @@ -637,6 +714,15 @@ message CancelActionByIdTagResponse { ResponseStatus status = 1; } +// Cancel a behavior in progress (v1.7) +message CancelBehaviorRequest { +} + +// Response from the robot to CancelBehaviorRequest. (v1.7) +message CancelBehaviorResponse { + ResponseStatus status = 1; +} + // GotoPose message GoToPoseRequest { float x_mm = 1; @@ -755,7 +841,7 @@ message TurnTowardsFaceResponse { } // GoToObject -message GoToObjectRequest { +message GoToObjectRequest { int32 object_id = 1; PathMotionProfile motion_prof = 2; float distance_from_object_origin_mm = 3; @@ -869,12 +955,12 @@ message VersionStateResponse { message SayTextRequest { string text = 1; bool use_vector_voice = 2; - float duration_scalar = 3; + float duration_scalar = 3; // Ranges from 0.05 (fast) to 20.0 (slow) + float pitch_scalar = 4; // Ranges from -1.0 (low) to +1.0 (high) (v1.7) } message SayTextResponse { - enum UtteranceState - { + enum UtteranceState { INVALID = 0; GENERATING = 1; READY = 2; @@ -1006,8 +1092,7 @@ message ExternalAudioStreamResponse { } } -enum MasterVolumeLevel -{ +enum MasterVolumeLevel { VOLUME_LOW = 0; VOLUME_MEDIUM_LOW = 1; VOLUME_MEDIUM = 2; @@ -1015,7 +1100,7 @@ enum MasterVolumeLevel VOLUME_HIGH = 4; } -message MasterVolumeRequest +message MasterVolumeRequest { MasterVolumeLevel volume_level = 1; } @@ -1063,7 +1148,7 @@ message EnableMotionDetectionResponse ResponseStatus status = 1; } -// When enabled, camera feed will appear on the robot's face, along with any +// When enabled, camera feed will appear on the robot's face, along with any // detections that are enabled from above messages message EnableMirrorModeRequest { @@ -1086,6 +1171,7 @@ message MirrorModeDisabled message EnableImageStreamingRequest { bool enable = 1; + bool enable_high_resolution = 2; } message EnableImageStreamingResponse @@ -1111,8 +1197,7 @@ message VisionModesAutoDisabled // One frame of image data and associated metadata message ImageChunk { - enum ImageEncoding - { + enum ImageEncoding { NONE_IMAGE_ENCODING = 0; RAW_GRAY = 1; // no compression RAW_RGB = 2; // no compression, just [RGBRGBRG...] @@ -1147,7 +1232,11 @@ message CameraFeedResponse { bytes data = 4; } -message CaptureSingleImageRequest {} +// Request for a single image from the robot. Default resolution is 640*360, +// enabling high resolution provides a 1280*720 image. +message CaptureSingleImageRequest { + bool enable_high_resolution = 1; // (v1.7) +} message CaptureSingleImageResponse { ResponseStatus status = 1; @@ -1166,6 +1255,43 @@ message SetEyeColorResponse { ResponseStatus status = 1; } +// (v1.7) +message CameraConfigRequest {} + +// (v1.7) +message CameraConfigResponse { + float focal_length_x = 1; + float focal_length_y = 2; + float center_x = 3; + float center_y = 4; + float fov_x = 5; // Full FOV in degrees + float fov_y = 6; // Full FOV in degrees + uint32 min_camera_exposure_time_ms = 7; + uint32 max_camera_exposure_time_ms = 8; + float min_camera_gain = 9; + float max_camera_gain = 10; +} + +// (v1.7) +message SetCameraSettingsRequest { + float gain = 1; + uint32 exposure_ms = 2; + bool enable_auto_exposure = 3; +} + +// (v1.7) +message SetCameraSettingsResponse { + ResponseStatus status = 1; + string status_message = 2; +} + +// Event sent when camera exposure settings change (v1.7) +message CameraSettingsUpdate { + float gain = 1; + uint32 exposure_ms = 2; + bool auto_exposure_enabled = 3; +} + message SDKInitializationRequest { string sdk_module_version = 1; string python_version = 2; @@ -1178,4 +1304,107 @@ message SDKInitializationResponse { ResponseStatus status = 1; } +message AppDisconnected{} + +message AppIntentRequest { + string intent = 1; + string param = 2; +} + +message AppIntentResponse { + ResponseStatus status = 1; +} + +message FeatureStatus { + string feature_name = 1; + string source = 2; // Voice, App, AI, Unknown +} + +message FeatureFlagRequest { + string feature_name = 1; +} +message FeatureFlagResponse { + ResponseStatus status = 1; + bool valid_feature = 2; + bool feature_enabled = 3; +} + +message FeatureFlagListRequest { + repeated string request_list = 1; +} + +message FeatureFlagListResponse { + ResponseStatus status = 1; + repeated string list = 2; +} + +// NOTE: must match attentionTransferTypes.clad +enum AttentionTransferReason { + Invalid = 0; + + NoCloudConnection = 1; + NoWifi = 2; + UnmatchedIntent = 3; +} + +message AttentionTransfer { + AttentionTransferReason reason = 1; + float seconds_ago = 2; +} + +message LatestAttentionTransferRequest { +} + +message LatestAttentionTransfer { + oneof oneof_message_type { + AttentionTransfer attention_transfer = 1; + } +} + +message LatestAttentionTransferResponse { + ResponseStatus status = 1; + LatestAttentionTransfer latest_attention_transfer = 2; +} + +message UpdateAndRestartRequest{} + +message UpdateAndRestartResponse{ + ResponseStatus status = 1; +} + + +message CheckUpdateStatusRequest{} + +message CheckUpdateStatusResponse{ + option(streamed) = true; + + ResponseStatus status = 1; + enum UpdateStatus { + NO_UPDATE = 0; + READY_TO_INSTALL = 1; + IN_PROGRESS_DOWNLOAD = 2; + } + UpdateStatus update_status = 2; + int64 expected = 3; + int64 progress = 4; + string update_version = 5; +} + +message CheckCloudRequest{} + +message CheckCloudResponse{ + ResponseStatus status = 1; + enum ConnectionCode { + UNKNOWN = 0; + AVAILABLE = 1; + BAD_CONNECTIVITY = 2; + FAILED_TLS = 3; + FAILED_AUTH = 4; + INSUFFICIENT_BANDWIDTH = 5; + } + ConnectionCode code = 2; + string status_message = 3; + int32 num_packets = 4; + int32 expected_packets = 5; +} diff --git a/anki_vector/messaging/messages_pb2.py b/anki_vector/messaging/messages_pb2.py index c30261a..879e5bf 100644 --- a/anki_vector/messaging/messages_pb2.py +++ b/anki_vector/messaging/messages_pb2.py @@ -22,7 +22,7 @@ name='anki_vector/messaging/messages.proto', package='Anki.Vector.external_interface', syntax='proto3', - serialized_pb=_b('\n$anki_vector/messaging/messages.proto\x12\x1e\x41nki.Vector.external_interface\x1a+anki_vector/messaging/response_status.proto\x1a&anki_vector/messaging/extensions.proto\"\x0f\n\rKeepAlivePing\" \n\x10\x41nimationTrigger\x12\x0c\n\x04name\x18\x01 \x01(\t\"\x19\n\tAnimation\x12\x0c\n\x04name\x18\x01 \x01(\t\"|\n\x12\x44riveWheelsRequest\x12\x17\n\x0fleft_wheel_mmps\x18\x01 \x01(\x02\x12\x18\n\x10right_wheel_mmps\x18\x02 \x01(\x02\x12\x18\n\x10left_wheel_mmps2\x18\x03 \x01(\x02\x12\x19\n\x11right_wheel_mmps2\x18\x04 \x01(\x02\"U\n\x13\x44riveWheelsResponse\x12>\n\x06status\x18\x01 \x01(\x0b\x32..Anki.Vector.external_interface.ResponseStatus\",\n\x0fMoveHeadRequest\x12\x19\n\x11speed_rad_per_sec\x18\x01 \x01(\x02\"R\n\x10MoveHeadResponse\x12>\n\x06status\x18\x01 \x01(\x0b\x32..Anki.Vector.external_interface.ResponseStatus\",\n\x0fMoveLiftRequest\x12\x19\n\x11speed_rad_per_sec\x18\x01 \x01(\x02\"R\n\x10MoveLiftResponse\x12>\n\x06status\x18\x01 \x01(\x0b\x32..Anki.Vector.external_interface.ResponseStatus\"\x16\n\x14StopAllMotorsRequest\"W\n\x15StopAllMotorsResponse\x12>\n\x06status\x18\x01 \x01(\x0b\x32..Anki.Vector.external_interface.ResponseStatus\"\xe1\x01\n\x1bPlayAnimationTriggerRequest\x12K\n\x11\x61nimation_trigger\x18\x01 \x01(\x0b\x32\x30.Anki.Vector.external_interface.AnimationTrigger\x12\r\n\x05loops\x18\x02 \x01(\r\x12\x15\n\ruse_lift_safe\x18\x03 \x01(\x08\x12\x19\n\x11ignore_body_track\x18\x04 \x01(\x08\x12\x19\n\x11ignore_head_track\x18\x05 \x01(\x08\x12\x19\n\x11ignore_lift_track\x18\x06 \x01(\x08\"\xb4\x01\n\x14PlayAnimationRequest\x12<\n\tanimation\x18\x01 \x01(\x0b\x32).Anki.Vector.external_interface.Animation\x12\r\n\x05loops\x18\x02 \x01(\r\x12\x19\n\x11ignore_body_track\x18\x03 \x01(\x08\x12\x19\n\x11ignore_head_track\x18\x04 \x01(\x08\x12\x19\n\x11ignore_lift_track\x18\x05 \x01(\x08\"\xd6\x01\n\x15PlayAnimationResponse\x12>\n\x06status\x18\x01 \x01(\x0b\x32..Anki.Vector.external_interface.ResponseStatus\x12?\n\x06result\x18\x02 \x01(\x0e\x32/.Anki.Vector.external_interface.BehaviorResults\x12<\n\tanimation\x18\x03 \x01(\x0b\x32).Anki.Vector.external_interface.Animation\"\x17\n\x15ListAnimationsRequest\"\x9c\x01\n\x16ListAnimationsResponse\x12>\n\x06status\x18\x01 \x01(\x0b\x32..Anki.Vector.external_interface.ResponseStatus\x12\x42\n\x0f\x61nimation_names\x18\x02 \x03(\x0b\x32).Anki.Vector.external_interface.Animation\"\x1e\n\x1cListAnimationTriggersRequest\"\xb2\x01\n\x1dListAnimationTriggersResponse\x12>\n\x06status\x18\x01 \x01(\x0b\x32..Anki.Vector.external_interface.ResponseStatus\x12Q\n\x17\x61nimation_trigger_names\x18\x02 \x03(\x0b\x32\x30.Anki.Vector.external_interface.AnimationTrigger\"_\n\x1a\x44isplayFaceImageRGBRequest\x12\x11\n\tface_data\x18\x01 \x01(\x0c\x12\x13\n\x0b\x64uration_ms\x18\x02 \x01(\r\x12\x19\n\x11interrupt_running\x18\x03 \x01(\x08\"]\n\x1b\x44isplayFaceImageRGBResponse\x12>\n\x06status\x18\x01 \x01(\x0b\x32..Anki.Vector.external_interface.ResponseStatus\"\x1b\n\x19MeetVictorFaceScanStarted\"\x1c\n\x1aMeetVictorFaceScanComplete\"\xbf\x02\n\x06Status\x12\x62\n\x1dmeet_victor_face_scan_started\x18\x02 \x01(\x0b\x32\x39.Anki.Vector.external_interface.MeetVictorFaceScanStartedH\x00\x12\x64\n\x1emeet_victor_face_scan_complete\x18\x03 \x01(\x0b\x32:.Anki.Vector.external_interface.MeetVictorFaceScanCompleteH\x00\x12\\\n\x19\x66\x61\x63\x65_enrollment_completed\x18\x04 \x01(\x0b\x32\x37.Anki.Vector.external_interface.FaceEnrollmentCompletedH\x00\x42\r\n\x0bstatus_type\"\xac\x01\n\x08WakeWord\x12H\n\x0fwake_word_begin\x18\x01 \x01(\x0b\x32-.Anki.Vector.external_interface.WakeWordBeginH\x00\x12\x44\n\rwake_word_end\x18\x02 \x01(\x0b\x32+.Anki.Vector.external_interface.WakeWordEndH\x00\x42\x10\n\x0ewake_word_type\"b\n\x11TimeStampedStatus\x12\x36\n\x06status\x18\x01 \x01(\x0b\x32&.Anki.Vector.external_interface.Status\x12\x15\n\rtimestamp_utc\x18\x02 \x01(\r\"p\n\nPoseStruct\x12\t\n\x01x\x18\x01 \x01(\x02\x12\t\n\x01y\x18\x02 \x01(\x02\x12\t\n\x01z\x18\x03 \x01(\x02\x12\n\n\x02q0\x18\x04 \x01(\x02\x12\n\n\x02q1\x18\x05 \x01(\x02\x12\n\n\x02q2\x18\x06 \x01(\x02\x12\n\n\x02q3\x18\x07 \x01(\x02\x12\x11\n\torigin_id\x18\x08 \x01(\r\",\n\tAccelData\x12\t\n\x01x\x18\x01 \x01(\x02\x12\t\n\x01y\x18\x02 \x01(\x02\x12\t\n\x01z\x18\x03 \x01(\x02\"+\n\x08GyroData\x12\t\n\x01x\x18\x01 \x01(\x02\x12\t\n\x01y\x18\x02 \x01(\x02\x12\t\n\x01z\x18\x03 \x01(\x02\"{\n\x08ProxData\x12\x13\n\x0b\x64istance_mm\x18\x01 \x01(\r\x12\x16\n\x0esignal_quality\x18\x02 \x01(\x02\x12\x14\n\x0cunobstructed\x18\x03 \x01(\x08\x12\x14\n\x0c\x66ound_object\x18\x04 \x01(\x08\x12\x16\n\x0eis_lift_in_fov\x18\x05 \x01(\x08\">\n\tTouchData\x12\x17\n\x0fraw_touch_value\x18\x01 \x01(\r\x12\x18\n\x10is_being_touched\x18\x02 \x01(\x08\"\x82\x05\n\nRobotState\x12\x38\n\x04pose\x18\x01 \x01(\x0b\x32*.Anki.Vector.external_interface.PoseStruct\x12\x16\n\x0epose_angle_rad\x18\x02 \x01(\x02\x12\x16\n\x0epose_pitch_rad\x18\x03 \x01(\x02\x12\x1d\n\x15left_wheel_speed_mmps\x18\x04 \x01(\x02\x12\x1e\n\x16right_wheel_speed_mmps\x18\x05 \x01(\x02\x12\x16\n\x0ehead_angle_rad\x18\x06 \x01(\x02\x12\x16\n\x0elift_height_mm\x18\x07 \x01(\x02\x12\x38\n\x05\x61\x63\x63\x65l\x18\x08 \x01(\x0b\x32).Anki.Vector.external_interface.AccelData\x12\x36\n\x04gyro\x18\t \x01(\x0b\x32(.Anki.Vector.external_interface.GyroData\x12\x1a\n\x12\x63\x61rrying_object_id\x18\n \x01(\x05\x12!\n\x19\x63\x61rrying_object_on_top_id\x18\x0b \x01(\x05\x12\x1f\n\x17head_tracking_object_id\x18\x0c \x01(\x05\x12\x1e\n\x16localized_to_object_id\x18\r \x01(\x05\x12\x1d\n\x15last_image_time_stamp\x18\x0e \x01(\r\x12\x0e\n\x06status\x18\x0f \x01(\r\x12;\n\tprox_data\x18\x10 \x01(\x0b\x32(.Anki.Vector.external_interface.ProxData\x12=\n\ntouch_data\x18\x11 \x01(\x0b\x32).Anki.Vector.external_interface.TouchData\"!\n\tCladPoint\x12\t\n\x01x\x18\x01 \x01(\x02\x12\t\n\x01y\x18\x02 \x01(\x02\"Q\n\x08\x43ladRect\x12\x12\n\nx_top_left\x18\x01 \x01(\x02\x12\x12\n\ny_top_left\x18\x02 \x01(\x02\x12\r\n\x05width\x18\x03 \x01(\x02\x12\x0e\n\x06height\x18\x04 \x01(\x02\"\x8a\x04\n\x11RobotObservedFace\x12\x0f\n\x07\x66\x61\x63\x65_id\x18\x01 \x01(\x05\x12\x11\n\ttimestamp\x18\x02 \x01(\r\x12\x38\n\x04pose\x18\x03 \x01(\x0b\x32*.Anki.Vector.external_interface.PoseStruct\x12:\n\x08img_rect\x18\x04 \x01(\x0b\x32(.Anki.Vector.external_interface.CladRect\x12\x0c\n\x04name\x18\x05 \x01(\t\x12\x44\n\nexpression\x18\x06 \x01(\x0e\x32\x30.Anki.Vector.external_interface.FacialExpression\x12\x19\n\x11\x65xpression_values\x18\x07 \x03(\r\x12;\n\x08left_eye\x18\x08 \x03(\x0b\x32).Anki.Vector.external_interface.CladPoint\x12<\n\tright_eye\x18\t \x03(\x0b\x32).Anki.Vector.external_interface.CladPoint\x12\x37\n\x04nose\x18\n \x03(\x0b\x32).Anki.Vector.external_interface.CladPoint\x12\x38\n\x05mouth\x18\x0b \x03(\x0b\x32).Anki.Vector.external_interface.CladPoint\"<\n\x1aRobotChangedObservedFaceID\x12\x0e\n\x06old_id\x18\x01 \x01(\x05\x12\x0e\n\x06new_id\x18\x02 \x01(\x05\"~\n\x17\x46\x61\x63\x65\x45nrollmentCompleted\x12\x44\n\x06result\x18\x01 \x01(\x0e\x32\x34.Anki.Vector.external_interface.FaceEnrollmentResult\x12\x0f\n\x07\x66\x61\x63\x65_id\x18\x02 \x01(\x05\x12\x0c\n\x04name\x18\x03 \x01(\t\"\x1d\n\x1b\x43\x61ncelFaceEnrollmentRequest\"^\n\x1c\x43\x61ncelFaceEnrollmentResponse\x12>\n\x06status\x18\x01 \x01(\x0b\x32..Anki.Vector.external_interface.ResponseStatus\"\x1d\n\x1bRequestEnrolledNamesRequest\"\xc2\x01\n\x0fLoadedKnownFace\x12$\n\x1cseconds_since_first_enrolled\x18\x01 \x01(\x03\x12\"\n\x1aseconds_since_last_updated\x18\x02 \x01(\x03\x12\x1f\n\x17seconds_since_last_seen\x18\x03 \x01(\x03\x12%\n\x1dlast_seen_seconds_since_epoch\x18\x04 \x01(\x03\x12\x0f\n\x07\x66\x61\x63\x65_id\x18\x05 \x01(\x05\x12\x0c\n\x04name\x18\x06 \x01(\t\"9\n\x18RobotRenamedEnrolledFace\x12\x0f\n\x07\x66\x61\x63\x65_id\x18\x01 \x01(\x05\x12\x0c\n\x04name\x18\x02 \x01(\t\"\x9e\x01\n\x1cRequestEnrolledNamesResponse\x12>\n\x06status\x18\x01 \x01(\x0b\x32..Anki.Vector.external_interface.ResponseStatus\x12>\n\x05\x66\x61\x63\x65s\x18\x02 \x03(\x0b\x32/.Anki.Vector.external_interface.LoadedKnownFace\"T\n\x1dUpdateEnrolledFaceByIDRequest\x12\x0f\n\x07\x66\x61\x63\x65_id\x18\x01 \x01(\x05\x12\x10\n\x08old_name\x18\x02 \x01(\t\x12\x10\n\x08new_name\x18\x03 \x01(\t\"`\n\x1eUpdateEnrolledFaceByIDResponse\x12>\n\x06status\x18\x01 \x01(\x0b\x32..Anki.Vector.external_interface.ResponseStatus\"/\n\x1c\x45raseEnrolledFaceByIDRequest\x12\x0f\n\x07\x66\x61\x63\x65_id\x18\x01 \x01(\x05\"_\n\x1d\x45raseEnrolledFaceByIDResponse\x12>\n\x06status\x18\x01 \x01(\x0b\x32..Anki.Vector.external_interface.ResponseStatus\"\x1e\n\x1c\x45raseAllEnrolledFacesRequest\"_\n\x1d\x45raseAllEnrolledFacesResponse\x12>\n\x06status\x18\x01 \x01(\x0b\x32..Anki.Vector.external_interface.ResponseStatus\"\x88\x01\n\x16SetFaceToEnrollRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x13\n\x0bobserved_id\x18\x02 \x01(\x05\x12\x0f\n\x07save_id\x18\x03 \x01(\x05\x12\x15\n\rsave_to_robot\x18\x04 \x01(\x08\x12\x10\n\x08say_name\x18\x05 \x01(\x08\x12\x11\n\tuse_music\x18\x06 \x01(\x08\"Y\n\x17SetFaceToEnrollResponse\x12>\n\x06status\x18\x01 \x01(\x0b\x32..Anki.Vector.external_interface.ResponseStatus\"\x18\n\x16\x44riveOffChargerRequest\"\x9a\x01\n\x17\x44riveOffChargerResponse\x12>\n\x06status\x18\x01 \x01(\x0b\x32..Anki.Vector.external_interface.ResponseStatus\x12?\n\x06result\x18\x02 \x01(\x0e\x32/.Anki.Vector.external_interface.BehaviorResults\"\x17\n\x15\x44riveOnChargerRequest\"\x99\x01\n\x16\x44riveOnChargerResponse\x12>\n\x06status\x18\x01 \x01(\x0b\x32..Anki.Vector.external_interface.ResponseStatus\x12?\n\x06result\x18\x02 \x01(\x0e\x32/.Anki.Vector.external_interface.BehaviorResults\"\x12\n\x10\x46indFacesRequest\"\x94\x01\n\x11\x46indFacesResponse\x12>\n\x06status\x18\x01 \x01(\x0b\x32..Anki.Vector.external_interface.ResponseStatus\x12?\n\x06result\x18\x02 \x01(\x0e\x32/.Anki.Vector.external_interface.BehaviorResults\"\x1a\n\x18LookAroundInPlaceRequest\"\x9c\x01\n\x19LookAroundInPlaceResponse\x12>\n\x06status\x18\x01 \x01(\x0b\x32..Anki.Vector.external_interface.ResponseStatus\x12?\n\x06result\x18\x02 \x01(\x0e\x32/.Anki.Vector.external_interface.BehaviorResults\"\x12\n\x10RollBlockRequest\"\x94\x01\n\x11RollBlockResponse\x12>\n\x06status\x18\x01 \x01(\x0b\x32..Anki.Vector.external_interface.ResponseStatus\x12?\n\x06result\x18\x02 \x01(\x0e\x32/.Anki.Vector.external_interface.BehaviorResults\"\x0f\n\rWakeWordBegin\"8\n\x0bWakeWordEnd\x12\x14\n\x0cintent_heard\x18\x01 \x01(\x08\x12\x13\n\x0bintent_json\x18\x02 \x01(\t\"2\n\nUserIntent\x12\x11\n\tintent_id\x18\x01 \x01(\r\x12\x11\n\tjson_data\x18\x02 \x01(\t\"n\n\tPhotoInfo\x12\x10\n\x08photo_id\x18\x01 \x01(\r\x12\x15\n\rtimestamp_utc\x18\x02 \x01(\r\x12\x1b\n\x13photo_copied_to_app\x18\x03 \x01(\x08\x12\x1b\n\x13thumb_copied_to_app\x18\x04 \x01(\x08\"\x13\n\x11PhotosInfoRequest\"\x94\x01\n\x12PhotosInfoResponse\x12>\n\x06status\x18\x01 \x01(\x0b\x32..Anki.Vector.external_interface.ResponseStatus\x12>\n\x0bphoto_infos\x18\x02 \x03(\x0b\x32).Anki.Vector.external_interface.PhotoInfo\" \n\x0cPhotoRequest\x12\x10\n\x08photo_id\x18\x01 \x01(\r\"6\n\x10PhotoPathMessage\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\x11\n\tfull_path\x18\x02 \x01(\t\"o\n\rPhotoResponse\x12>\n\x06status\x18\x01 \x01(\x0b\x32..Anki.Vector.external_interface.ResponseStatus\x12\x0f\n\x07success\x18\x02 \x01(\x08\x12\r\n\x05image\x18\x03 \x01(\x0c\"$\n\x10ThumbnailRequest\x12\x10\n\x08photo_id\x18\x01 \x01(\r\":\n\x14ThumbnailPathMessage\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\x11\n\tfull_path\x18\x02 \x01(\t\"s\n\x11ThumbnailResponse\x12>\n\x06status\x18\x01 \x01(\x0b\x32..Anki.Vector.external_interface.ResponseStatus\x12\x0f\n\x07success\x18\x02 \x01(\x08\x12\r\n\x05image\x18\x03 \x01(\x0c\"&\n\x12\x44\x65letePhotoRequest\x12\x10\n\x08photo_id\x18\x01 \x01(\r\"f\n\x13\x44\x65letePhotoResponse\x12>\n\x06status\x18\x01 \x01(\x0b\x32..Anki.Vector.external_interface.ResponseStatus\x12\x0f\n\x07success\x18\x02 \x01(\x08\"\x1e\n\nPhotoTaken\x12\x10\n\x08photo_id\x18\x01 \x01(\r\"\xc1\x02\n\x11PathMotionProfile\x12\x12\n\nspeed_mmps\x18\x01 \x01(\x02\x12\x13\n\x0b\x61\x63\x63\x65l_mmps2\x18\x02 \x01(\x02\x12\x13\n\x0b\x64\x65\x63\x65l_mmps2\x18\x03 \x01(\x02\x12$\n\x1cpoint_turn_speed_rad_per_sec\x18\x04 \x01(\x02\x12%\n\x1dpoint_turn_accel_rad_per_sec2\x18\x05 \x01(\x02\x12%\n\x1dpoint_turn_decel_rad_per_sec2\x18\x06 \x01(\x02\x12\x17\n\x0f\x64ock_speed_mmps\x18\x07 \x01(\x02\x12\x18\n\x10\x64ock_accel_mmps2\x18\x08 \x01(\x02\x12\x18\n\x10\x64ock_decel_mmps2\x18\t \x01(\x02\x12\x1a\n\x12reverse_speed_mmps\x18\n \x01(\x02\x12\x11\n\tis_custom\x18\x0b \x01(\x08\"\xf8\x0c\n\x0c\x41\x63tionResult\x12K\n\x04\x63ode\x18\x01 \x01(\x0e\x32=.Anki.Vector.external_interface.ActionResult.ActionResultCode\"\x9a\x0c\n\x10\x41\x63tionResultCode\x12\x19\n\x15\x41\x43TION_RESULT_SUCCESS\x10\x00\x12\x1c\n\x15\x41\x43TION_RESULT_RUNNING\x10\x80\x80\x80\x08\x12,\n%ACTION_RESULT_CANCELLED_WHILE_RUNNING\x10\x80\x80\x80\x10\x12\x12\n\x0bNOT_STARTED\x10\x81\x80\x80\x10\x12\x0c\n\x05\x41\x42ORT\x10\x80\x80\x80\x18\x12\x13\n\x0c\x41NIM_ABORTED\x10\x81\x80\x80\x18\x12\x11\n\nBAD_MARKER\x10\x82\x80\x80\x18\x12\x16\n\x0f\x42\x41\x44_MESSAGE_TAG\x10\x83\x80\x80\x18\x12\x11\n\nBAD_OBJECT\x10\x84\x80\x80\x18\x12\x0f\n\x08\x42\x41\x44_POSE\x10\x85\x80\x80\x18\x12\x0e\n\x07\x42\x41\x44_TAG\x10\x86\x80\x80\x18\x12\x1e\n\x17\x43HARGER_UNPLUGGED_ABORT\x10\x87\x80\x80\x18\x12!\n\x1a\x43LIFF_ALIGN_FAILED_TIMEOUT\x10\x88\x80\x80\x18\x12$\n\x1d\x43LIFF_ALIGN_FAILED_NO_TURNING\x10\x89\x80\x80\x18\x12&\n\x1f\x43LIFF_ALIGN_FAILED_OVER_TURNING\x10\x8a\x80\x80\x18\x12\"\n\x1b\x43LIFF_ALIGN_FAILED_NO_WHITE\x10\x8b\x80\x80\x18\x12!\n\x1a\x43LIFF_ALIGN_FAILED_STOPPED\x10\x8c\x80\x80\x18\x12!\n\x1a\x46\x41ILED_SETTING_CALIBRATION\x10\x8d\x80\x80\x18\x12(\n!FOLLOWING_PATH_BUT_NOT_TRAVERSING\x10\x8e\x80\x80\x18\x12\x12\n\x0bINTERRUPTED\x10\x8f\x80\x80\x18\x12\x1f\n\x18INVALID_OFF_TREADS_STATE\x10\x90\x80\x80\x18\x12\x19\n\x12MISMATCHED_UP_AXIS\x10\x91\x80\x80\x18\x12\x13\n\x0cNO_ANIM_NAME\x10\x92\x80\x80\x18\x12\x16\n\x0fNO_DISTANCE_SET\x10\x93\x80\x80\x18\x12\x0e\n\x07NO_FACE\x10\x94\x80\x80\x18\x12\x12\n\x0bNO_GOAL_SET\x10\x95\x80\x80\x18\x12\x19\n\x12NO_PREACTION_POSES\x10\x96\x80\x80\x18\x12 \n\x19NOT_CARRYING_OBJECT_ABORT\x10\x97\x80\x80\x18\x12\x1b\n\x14NOT_ON_CHARGER_ABORT\x10\x98\x80\x80\x18\x12\x15\n\x0eNULL_SUBACTION\x10\x99\x80\x80\x18\x12!\n\x1aPATH_PLANNING_FAILED_ABORT\x10\x9a\x80\x80\x18\x12(\n!PICKUP_OBJECT_UNEXPECTEDLY_MOVING\x10\x9b\x80\x80\x18\x12#\n\x1cSEND_MESSAGE_TO_ROBOT_FAILED\x10\x9c\x80\x80\x18\x12\x1c\n\x15STILL_CARRYING_OBJECT\x10\x9d\x80\x80\x18\x12\x0e\n\x07TIMEOUT\x10\x9e\x80\x80\x18\x12\x14\n\rTRACKS_LOCKED\x10\x9f\x80\x80\x18\x12\x1d\n\x16UNEXPECTED_DOCK_ACTION\x10\xa0\x80\x80\x18\x12\x18\n\x11UNKNOWN_TOOL_CODE\x10\xa1\x80\x80\x18\x12\x1c\n\x15UPDATE_DERIVED_FAILED\x10\xa2\x80\x80\x18\x12 \n\x19VISUAL_OBSERVATION_FAILED\x10\xa3\x80\x80\x18\x12 \n\x19SHOULDNT_DRIVE_ON_CHARGER\x10\xa4\x80\x80\x18\x12\x0c\n\x05RETRY\x10\x80\x80\x80 \x12#\n\x1c\x44ID_NOT_REACH_PREACTION_POSE\x10\x81\x80\x80 \x12\x1d\n\x16\x46\x41ILED_TRAVERSING_PATH\x10\x82\x80\x80 \x12!\n\x1aLAST_PICK_AND_PLACE_FAILED\x10\x83\x80\x80 \x12$\n\x1dMOTOR_STOPPED_MAKING_PROGRESS\x10\x84\x80\x80 \x12 \n\x19NOT_CARRYING_OBJECT_RETRY\x10\x85\x80\x80 \x12\x1b\n\x14NOT_ON_CHARGER_RETRY\x10\x86\x80\x80 \x12!\n\x1aPATH_PLANNING_FAILED_RETRY\x10\x87\x80\x80 \x12\x1e\n\x17PLACEMENT_GOAL_NOT_FREE\x10\x88\x80\x80 \x12,\n%PICKUP_OBJECT_UNEXPECTEDLY_NOT_MOVING\x10\x89\x80\x80 \x12\x17\n\x10STILL_ON_CHARGER\x10\x8a\x80\x80 \x12\x1d\n\x16UNEXPECTED_PITCH_ANGLE\x10\x8b\x80\x80 \",\n\x1a\x43\x61ncelActionByIdTagRequest\x12\x0e\n\x06id_tag\x18\x01 \x01(\r\"]\n\x1b\x43\x61ncelActionByIdTagResponse\x12>\n\x06status\x18\x01 \x01(\x0b\x32..Anki.Vector.external_interface.ResponseStatus\"\xa7\x01\n\x0fGoToPoseRequest\x12\x0c\n\x04x_mm\x18\x01 \x01(\x02\x12\x0c\n\x04y_mm\x18\x02 \x01(\x02\x12\x0b\n\x03rad\x18\x03 \x01(\x02\x12\x46\n\x0bmotion_prof\x18\x04 \x01(\x0b\x32\x31.Anki.Vector.external_interface.PathMotionProfile\x12\x0e\n\x06id_tag\x18\x05 \x01(\x05\x12\x13\n\x0bnum_retries\x18\x06 \x01(\x05\"\x90\x01\n\x10GoToPoseResponse\x12>\n\x06status\x18\x01 \x01(\x0b\x32..Anki.Vector.external_interface.ResponseStatus\x12<\n\x06result\x18\x02 \x01(\x0b\x32,.Anki.Vector.external_interface.ActionResult\"\xd0\x02\n\x13\x44ockWithCubeRequest\x12\x11\n\tobject_id\x18\x01 \x01(\x05\x12\x1f\n\x17\x64istance_from_marker_mm\x18\x02 \x01(\x02\x12\x1a\n\x12\x61pproach_angle_rad\x18\x03 \x01(\x02\x12\x45\n\x0e\x61lignment_type\x18\x04 \x01(\x0e\x32-.Anki.Vector.external_interface.AlignmentType\x12\x1a\n\x12use_approach_angle\x18\x05 \x01(\x08\x12\x19\n\x11use_pre_dock_pose\x18\x06 \x01(\x08\x12\x46\n\x0bmotion_prof\x18\x07 \x01(\x0b\x32\x31.Anki.Vector.external_interface.PathMotionProfile\x12\x0e\n\x06id_tag\x18\x08 \x01(\x05\x12\x13\n\x0bnum_retries\x18\t \x01(\x05\"\x94\x01\n\x14\x44ockWithCubeResponse\x12>\n\x06status\x18\x01 \x01(\x0b\x32..Anki.Vector.external_interface.ResponseStatus\x12<\n\x06result\x18\x02 \x01(\x0b\x32,.Anki.Vector.external_interface.ActionResult\"\x7f\n\x14\x44riveStraightRequest\x12\x12\n\nspeed_mmps\x18\x01 \x01(\x02\x12\x0f\n\x07\x64ist_mm\x18\x02 \x01(\x02\x12\x1d\n\x15should_play_animation\x18\x03 \x01(\x08\x12\x0e\n\x06id_tag\x18\x04 \x01(\x05\x12\x13\n\x0bnum_retries\x18\x05 \x01(\x05\"\x95\x01\n\x15\x44riveStraightResponse\x12>\n\x06status\x18\x01 \x01(\x0b\x32..Anki.Vector.external_interface.ResponseStatus\x12<\n\x06result\x18\x02 \x01(\x0b\x32,.Anki.Vector.external_interface.ActionResult\"\xa9\x01\n\x12TurnInPlaceRequest\x12\x11\n\tangle_rad\x18\x01 \x01(\x02\x12\x19\n\x11speed_rad_per_sec\x18\x02 \x01(\x02\x12\x1a\n\x12\x61\x63\x63\x65l_rad_per_sec2\x18\x03 \x01(\x02\x12\x0f\n\x07tol_rad\x18\x04 \x01(\x02\x12\x13\n\x0bis_absolute\x18\x05 \x01(\r\x12\x0e\n\x06id_tag\x18\x06 \x01(\x05\x12\x13\n\x0bnum_retries\x18\x07 \x01(\x05\"\x93\x01\n\x13TurnInPlaceResponse\x12>\n\x06status\x18\x01 \x01(\x0b\x32..Anki.Vector.external_interface.ResponseStatus\x12<\n\x06result\x18\x02 \x01(\x0b\x32,.Anki.Vector.external_interface.ActionResult\"\x9e\x01\n\x13SetHeadAngleRequest\x12\x11\n\tangle_rad\x18\x01 \x01(\x02\x12\x1d\n\x15max_speed_rad_per_sec\x18\x02 \x01(\x02\x12\x1a\n\x12\x61\x63\x63\x65l_rad_per_sec2\x18\x03 \x01(\x02\x12\x14\n\x0c\x64uration_sec\x18\x04 \x01(\x02\x12\x0e\n\x06id_tag\x18\x05 \x01(\x05\x12\x13\n\x0bnum_retries\x18\x06 \x01(\x05\"\x94\x01\n\x14SetHeadAngleResponse\x12>\n\x06status\x18\x01 \x01(\x0b\x32..Anki.Vector.external_interface.ResponseStatus\x12<\n\x06result\x18\x02 \x01(\x0b\x32,.Anki.Vector.external_interface.ActionResult\"\x9f\x01\n\x14SetLiftHeightRequest\x12\x11\n\theight_mm\x18\x01 \x01(\x02\x12\x1d\n\x15max_speed_rad_per_sec\x18\x02 \x01(\x02\x12\x1a\n\x12\x61\x63\x63\x65l_rad_per_sec2\x18\x03 \x01(\x02\x12\x14\n\x0c\x64uration_sec\x18\x04 \x01(\x02\x12\x0e\n\x06id_tag\x18\x05 \x01(\x05\x12\x13\n\x0bnum_retries\x18\x06 \x01(\x05\"\x95\x01\n\x15SetLiftHeightResponse\x12>\n\x06status\x18\x01 \x01(\x0b\x32..Anki.Vector.external_interface.ResponseStatus\x12<\n\x06result\x18\x02 \x01(\x0b\x32,.Anki.Vector.external_interface.ActionResult\"j\n\x16TurnTowardsFaceRequest\x12\x0f\n\x07\x66\x61\x63\x65_id\x18\x01 \x01(\x05\x12\x1a\n\x12max_turn_angle_rad\x18\x02 \x01(\x02\x12\x0e\n\x06id_tag\x18\x03 \x01(\x05\x12\x13\n\x0bnum_retries\x18\x04 \x01(\x05\"\x97\x01\n\x17TurnTowardsFaceResponse\x12>\n\x06status\x18\x01 \x01(\x0b\x32..Anki.Vector.external_interface.ResponseStatus\x12<\n\x06result\x18\x02 \x01(\x0b\x32,.Anki.Vector.external_interface.ActionResult\"\xd6\x01\n\x11GoToObjectRequest\x12\x11\n\tobject_id\x18\x01 \x01(\x05\x12\x46\n\x0bmotion_prof\x18\x02 \x01(\x0b\x32\x31.Anki.Vector.external_interface.PathMotionProfile\x12&\n\x1e\x64istance_from_object_origin_mm\x18\x03 \x01(\x02\x12\x19\n\x11use_pre_dock_pose\x18\x04 \x01(\x08\x12\x0e\n\x06id_tag\x18\x05 \x01(\x05\x12\x13\n\x0bnum_retries\x18\x06 \x01(\x05\"\x92\x01\n\x12GoToObjectResponse\x12>\n\x06status\x18\x01 \x01(\x0b\x32..Anki.Vector.external_interface.ResponseStatus\x12<\n\x06result\x18\x02 \x01(\x0b\x32,.Anki.Vector.external_interface.ActionResult\"\xe6\x01\n\x11RollObjectRequest\x12\x11\n\tobject_id\x18\x01 \x01(\x05\x12\x46\n\x0bmotion_prof\x18\x02 \x01(\x0b\x32\x31.Anki.Vector.external_interface.PathMotionProfile\x12\x1a\n\x12\x61pproach_angle_rad\x18\x03 \x01(\x02\x12\x1a\n\x12use_approach_angle\x18\x04 \x01(\x08\x12\x19\n\x11use_pre_dock_pose\x18\x05 \x01(\x08\x12\x0e\n\x06id_tag\x18\x06 \x01(\x05\x12\x13\n\x0bnum_retries\x18\x07 \x01(\x05\"\x92\x01\n\x12RollObjectResponse\x12>\n\x06status\x18\x01 \x01(\x0b\x32..Anki.Vector.external_interface.ResponseStatus\x12<\n\x06result\x18\x02 \x01(\x0b\x32,.Anki.Vector.external_interface.ActionResult\"\xe7\x01\n\x12PopAWheelieRequest\x12\x11\n\tobject_id\x18\x01 \x01(\x05\x12\x46\n\x0bmotion_prof\x18\x02 \x01(\x0b\x32\x31.Anki.Vector.external_interface.PathMotionProfile\x12\x1a\n\x12\x61pproach_angle_rad\x18\x03 \x01(\x02\x12\x1a\n\x12use_approach_angle\x18\x04 \x01(\x08\x12\x19\n\x11use_pre_dock_pose\x18\x05 \x01(\x08\x12\x0e\n\x06id_tag\x18\x06 \x01(\x05\x12\x13\n\x0bnum_retries\x18\x07 \x01(\x05\"\x93\x01\n\x13PopAWheelieResponse\x12>\n\x06status\x18\x01 \x01(\x0b\x32..Anki.Vector.external_interface.ResponseStatus\x12<\n\x06result\x18\x02 \x01(\x0b\x32,.Anki.Vector.external_interface.ActionResult\"\xe8\x01\n\x13PickupObjectRequest\x12\x11\n\tobject_id\x18\x01 \x01(\x05\x12\x46\n\x0bmotion_prof\x18\x02 \x01(\x0b\x32\x31.Anki.Vector.external_interface.PathMotionProfile\x12\x1a\n\x12\x61pproach_angle_rad\x18\x03 \x01(\x02\x12\x1a\n\x12use_approach_angle\x18\x04 \x01(\x08\x12\x19\n\x11use_pre_dock_pose\x18\x05 \x01(\x08\x12\x0e\n\x06id_tag\x18\x06 \x01(\x05\x12\x13\n\x0bnum_retries\x18\x07 \x01(\x05\"\x94\x01\n\x14PickupObjectResponse\x12>\n\x06status\x18\x01 \x01(\x0b\x32..Anki.Vector.external_interface.ResponseStatus\x12<\n\x06result\x18\x02 \x01(\x0b\x32,.Anki.Vector.external_interface.ActionResult\"E\n\x1ePlaceObjectOnGroundHereRequest\x12\x0e\n\x06id_tag\x18\x01 \x01(\x05\x12\x13\n\x0bnum_retries\x18\x02 \x01(\x05\"\x9f\x01\n\x1fPlaceObjectOnGroundHereResponse\x12>\n\x06status\x18\x01 \x01(\x0b\x32..Anki.Vector.external_interface.ResponseStatus\x12<\n\x06result\x18\x02 \x01(\x0b\x32,.Anki.Vector.external_interface.ActionResult\"\x15\n\x13\x42\x61tteryStateRequest\"\xc9\x02\n\x14\x42\x61tteryStateResponse\x12>\n\x06status\x18\x01 \x01(\x0b\x32..Anki.Vector.external_interface.ResponseStatus\x12\x43\n\rbattery_level\x18\x02 \x01(\x0e\x32,.Anki.Vector.external_interface.BatteryLevel\x12\x15\n\rbattery_volts\x18\x03 \x01(\x02\x12\x13\n\x0bis_charging\x18\x04 \x01(\x08\x12\x1e\n\x16is_on_charger_platform\x18\x05 \x01(\x08\x12\x1d\n\x15suggested_charger_sec\x18\x06 \x01(\x02\x12\x41\n\x0c\x63ube_battery\x18\x07 \x01(\x0b\x32+.Anki.Vector.external_interface.CubeBattery\"\xd3\x01\n\x0b\x43ubeBattery\x12K\n\x05level\x18\x01 \x01(\x0e\x32<.Anki.Vector.external_interface.CubeBattery.CubeBatteryLevel\x12\x12\n\nfactory_id\x18\x02 \x01(\t\x12\x15\n\rbattery_volts\x18\x03 \x01(\x02\x12#\n\x1btime_since_last_reading_sec\x18\x04 \x01(\x02\"\'\n\x10\x43ubeBatteryLevel\x12\x07\n\x03Low\x10\x00\x12\n\n\x06Normal\x10\x01\"\x15\n\x13VersionStateRequest\"\x83\x01\n\x14VersionStateResponse\x12>\n\x06status\x18\x01 \x01(\x0b\x32..Anki.Vector.external_interface.ResponseStatus\x12\x12\n\nos_version\x18\x02 \x01(\t\x12\x17\n\x0f\x65ngine_build_id\x18\x03 \x01(\t\"Q\n\x0eSayTextRequest\x12\x0c\n\x04text\x18\x01 \x01(\t\x12\x18\n\x10use_vector_voice\x18\x02 \x01(\x08\x12\x17\n\x0f\x64uration_scalar\x18\x03 \x01(\x02\"\xf5\x01\n\x0fSayTextResponse\x12>\n\x06status\x18\x01 \x01(\x0b\x32..Anki.Vector.external_interface.ResponseStatus\x12M\n\x05state\x18\x02 \x01(\x0e\x32>.Anki.Vector.external_interface.SayTextResponse.UtteranceState\"S\n\x0eUtteranceState\x12\x0b\n\x07INVALID\x10\x00\x12\x0e\n\nGENERATING\x10\x01\x12\t\n\x05READY\x10\x02\x12\x0b\n\x07PLAYING\x10\x03\x12\x0c\n\x08\x46INISHED\x10\x04\"\x9b\x01\n\x0fStimulationInfo\x12\x16\n\x0e\x65motion_events\x18\x01 \x03(\t\x12\r\n\x05value\x18\x02 \x01(\x02\x12\x10\n\x08velocity\x18\x03 \x01(\x02\x12\r\n\x05\x61\x63\x63\x65l\x18\x04 \x01(\x02\x12\x1a\n\x12value_before_event\x18\x05 \x01(\x02\x12\x11\n\tmin_value\x18\x06 \x01(\x02\x12\x11\n\tmax_value\x18\x07 \x01(\x02\"Y\n\x14\x41udioSendModeRequest\x12\x41\n\x04mode\x18\x01 \x01(\x0e\x32\x33.Anki.Vector.external_interface.AudioProcessingMode\"Y\n\x14\x41udioSendModeChanged\x12\x41\n\x04mode\x18\x01 \x01(\x0e\x32\x33.Anki.Vector.external_interface.AudioProcessingMode\"\xe8\x01\n\nAudioChunk\x12\x18\n\x10robot_time_stamp\x18\x01 \x01(\r\x12\x10\n\x08group_id\x18\x02 \x01(\r\x12\x10\n\x08\x63hunk_id\x18\x03 \x01(\r\x12\x19\n\x11\x61udio_chunk_count\x18\x04 \x01(\r\x12\x14\n\x0csignal_power\x18\x05 \x01(\x0c\x12\x1b\n\x13\x64irection_strengths\x18\x06 \x01(\x0c\x12\x18\n\x10source_direction\x18\x07 \x01(\r\x12\x19\n\x11source_confidence\x18\x08 \x01(\r\x12\x19\n\x11noise_floor_power\x18\t \x01(\r\"\x12\n\x10\x41udioFeedRequest\"\xc8\x01\n\x11\x41udioFeedResponse\x12\x18\n\x10robot_time_stamp\x18\x01 \x01(\r\x12\x10\n\x08group_id\x18\x02 \x01(\r\x12\x14\n\x0csignal_power\x18\x03 \x01(\x0c\x12\x1b\n\x13\x64irection_strengths\x18\x04 \x01(\x0c\x12\x18\n\x10source_direction\x18\x05 \x01(\r\x12\x19\n\x11source_confidence\x18\x06 \x01(\r\x12\x19\n\x11noise_floor_power\x18\x07 \x01(\r:\x04\x80\xa6\x1d\x01\"L\n\x1a\x45xternalAudioStreamPrepare\x12\x18\n\x10\x61udio_frame_rate\x18\x01 \x01(\r\x12\x14\n\x0c\x61udio_volume\x18\x02 \x01(\r\"W\n\x18\x45xternalAudioStreamChunk\x12\x1e\n\x16\x61udio_chunk_size_bytes\x18\x01 \x01(\r\x12\x1b\n\x13\x61udio_chunk_samples\x18\x02 \x01(\x0c\"\x1d\n\x1b\x45xternalAudioStreamComplete\"\x1b\n\x19\x45xternalAudioStreamCancel\"\x9e\x03\n\x1a\x45xternalAudioStreamRequest\x12Z\n\x14\x61udio_stream_prepare\x18\x01 \x01(\x0b\x32:.Anki.Vector.external_interface.ExternalAudioStreamPrepareH\x00\x12V\n\x12\x61udio_stream_chunk\x18\x02 \x01(\x0b\x32\x38.Anki.Vector.external_interface.ExternalAudioStreamChunkH\x00\x12\\\n\x15\x61udio_stream_complete\x18\x03 \x01(\x0b\x32;.Anki.Vector.external_interface.ExternalAudioStreamCompleteH\x00\x12X\n\x13\x61udio_stream_cancel\x18\x04 \x01(\x0b\x32\x39.Anki.Vector.external_interface.ExternalAudioStreamCancelH\x00\x42\x14\n\x12\x61udio_request_type\"%\n#ExternalAudioStreamPlaybackComplete\"$\n\"ExternalAudioStreamPlaybackFailure\"\\\n ExternalAudioStreamBufferOverrun\x12\x1a\n\x12\x61udio_samples_sent\x18\x01 \x01(\r\x12\x1c\n\x14\x61udio_samples_played\x18\x02 \x01(\r\"\xf9\x02\n\x1b\x45xternalAudioStreamResponse\x12m\n\x1e\x61udio_stream_playback_complete\x18\x01 \x01(\x0b\x32\x43.Anki.Vector.external_interface.ExternalAudioStreamPlaybackCompleteH\x00\x12g\n\x1b\x61udio_stream_buffer_overrun\x18\x02 \x01(\x0b\x32@.Anki.Vector.external_interface.ExternalAudioStreamBufferOverrunH\x00\x12k\n\x1d\x61udio_stream_playback_failyer\x18\x03 \x01(\x0b\x32\x42.Anki.Vector.external_interface.ExternalAudioStreamPlaybackFailureH\x00\x42\x15\n\x13\x61udio_response_type\"^\n\x13MasterVolumeRequest\x12G\n\x0cvolume_level\x18\x01 \x01(\x0e\x32\x31.Anki.Vector.external_interface.MasterVolumeLevel\"V\n\x14MasterVolumeResponse\x12>\n\x06status\x18\x01 \x01(\x0b\x32..Anki.Vector.external_interface.ResponseStatus\".\n\x1c\x45nableMarkerDetectionRequest\x12\x0e\n\x06\x65nable\x18\x01 \x01(\x08\"_\n\x1d\x45nableMarkerDetectionResponse\x12>\n\x06status\x18\x01 \x01(\x0b\x32..Anki.Vector.external_interface.ResponseStatus\"\xb1\x01\n\x1a\x45nableFaceDetectionRequest\x12\x0e\n\x06\x65nable\x18\x01 \x01(\x08\x12\x1e\n\x16\x65nable_smile_detection\x18\x02 \x01(\x08\x12$\n\x1c\x65nable_expression_estimation\x18\x03 \x01(\x08\x12\x1e\n\x16\x65nable_blink_detection\x18\x04 \x01(\x08\x12\x1d\n\x15\x65nable_gaze_detection\x18\x05 \x01(\x08\"]\n\x1b\x45nableFaceDetectionResponse\x12>\n\x06status\x18\x01 \x01(\x0b\x32..Anki.Vector.external_interface.ResponseStatus\".\n\x1c\x45nableMotionDetectionRequest\x12\x0e\n\x06\x65nable\x18\x01 \x01(\x08\"_\n\x1d\x45nableMotionDetectionResponse\x12>\n\x06status\x18\x01 \x01(\x0b\x32..Anki.Vector.external_interface.ResponseStatus\")\n\x17\x45nableMirrorModeRequest\x12\x0e\n\x06\x65nable\x18\x01 \x01(\x08\"Z\n\x18\x45nableMirrorModeResponse\x12>\n\x06status\x18\x01 \x01(\x0b\x32..Anki.Vector.external_interface.ResponseStatus\"\x14\n\x12MirrorModeDisabled\"-\n\x1b\x45nableImageStreamingRequest\x12\x0e\n\x06\x65nable\x18\x01 \x01(\x08\"^\n\x1c\x45nableImageStreamingResponse\x12>\n\x06status\x18\x01 \x01(\x0b\x32..Anki.Vector.external_interface.ResponseStatus\" \n\x1eIsImageStreamingEnabledRequest\"E\n\x1fIsImageStreamingEnabledResponse\x12\"\n\x1ais_image_streaming_enabled\x18\x01 \x01(\x08\"\x19\n\x17VisionModesAutoDisabled\"\xd1\x03\n\nImageChunk\x12\x18\n\x10\x66rame_time_stamp\x18\x01 \x01(\r\x12\x10\n\x08image_id\x18\x02 \x01(\r\x12\r\n\x05width\x18\x03 \x01(\r\x12\x0e\n\x06height\x18\x04 \x01(\r\x12P\n\x0eimage_encoding\x18\x05 \x01(\x0e\x32\x38.Anki.Vector.external_interface.ImageChunk.ImageEncoding\x12\x15\n\rdisplay_index\x18\x06 \x01(\r\x12\x19\n\x11image_chunk_count\x18\x07 \x01(\r\x12\x10\n\x08\x63hunk_id\x18\x08 \x01(\r\x12\x0c\n\x04\x64\x61ta\x18\t \x01(\x0c\"\xd3\x01\n\rImageEncoding\x12\x17\n\x13NONE_IMAGE_ENCODING\x10\x00\x12\x0c\n\x08RAW_GRAY\x10\x01\x12\x0b\n\x07RAW_RGB\x10\x02\x12\x08\n\x04YUYV\x10\x03\x12\x0c\n\x08YUV420SP\x10\x04\x12\t\n\x05\x42\x41YER\x10\x05\x12\r\n\tJPEG_GRAY\x10\x06\x12\x0e\n\nJPEG_COLOR\x10\x07\x12\x19\n\x15JPEG_COLOR_HALF_WIDTH\x10\x08\x12\x17\n\x13JPEG_MINIMIZED_GRAY\x10\t\x12\x18\n\x14JPEG_MINIMIZED_COLOR\x10\n\"\x13\n\x11\x43\x61meraFeedRequest\"\xa6\x01\n\x12\x43\x61meraFeedResponse\x12\x18\n\x10\x66rame_time_stamp\x18\x01 \x01(\r\x12\x10\n\x08image_id\x18\x02 \x01(\r\x12P\n\x0eimage_encoding\x18\x03 \x01(\x0e\x32\x38.Anki.Vector.external_interface.ImageChunk.ImageEncoding\x12\x0c\n\x04\x64\x61ta\x18\x04 \x01(\x0c:\x04\x80\xa6\x1d\x01\"\x1b\n\x19\x43\x61ptureSingleImageRequest\"\xe8\x01\n\x1a\x43\x61ptureSingleImageResponse\x12>\n\x06status\x18\x01 \x01(\x0b\x32..Anki.Vector.external_interface.ResponseStatus\x12\x18\n\x10\x66rame_time_stamp\x18\x02 \x01(\r\x12\x10\n\x08image_id\x18\x03 \x01(\r\x12P\n\x0eimage_encoding\x18\x04 \x01(\x0e\x32\x38.Anki.Vector.external_interface.ImageChunk.ImageEncoding\x12\x0c\n\x04\x64\x61ta\x18\x05 \x01(\x0c\"5\n\x12SetEyeColorRequest\x12\x0b\n\x03hue\x18\x01 \x01(\x02\x12\x12\n\nsaturation\x18\x02 \x01(\x02\"U\n\x13SetEyeColorResponse\x12>\n\x06status\x18\x01 \x01(\x0b\x32..Anki.Vector.external_interface.ResponseStatus\"\x96\x01\n\x18SDKInitializationRequest\x12\x1a\n\x12sdk_module_version\x18\x01 \x01(\t\x12\x16\n\x0epython_version\x18\x02 \x01(\t\x12\x1d\n\x15python_implementation\x18\x03 \x01(\t\x12\x12\n\nos_version\x18\x04 \x01(\t\x12\x13\n\x0b\x63pu_version\x18\x05 \x01(\t\"[\n\x19SDKInitializationResponse\x12>\n\x06status\x18\x01 \x01(\x0b\x32..Anki.Vector.external_interface.ResponseStatus*\xdc\x04\n\x0bRobotStatus\x12\x15\n\x11ROBOT_STATUS_NONE\x10\x00\x12\x1a\n\x16ROBOT_STATUS_IS_MOVING\x10\x01\x12\"\n\x1eROBOT_STATUS_IS_CARRYING_BLOCK\x10\x02\x12&\n\"ROBOT_STATUS_IS_PICKING_OR_PLACING\x10\x04\x12\x1d\n\x19ROBOT_STATUS_IS_PICKED_UP\x10\x08\x12\"\n\x1eROBOT_STATUS_IS_BUTTON_PRESSED\x10\x10\x12\x1b\n\x17ROBOT_STATUS_IS_FALLING\x10 \x12\x1d\n\x19ROBOT_STATUS_IS_ANIMATING\x10@\x12\x1c\n\x17ROBOT_STATUS_IS_PATHING\x10\x80\x01\x12\x1d\n\x18ROBOT_STATUS_LIFT_IN_POS\x10\x80\x02\x12\x1d\n\x18ROBOT_STATUS_HEAD_IN_POS\x10\x80\x04\x12!\n\x1cROBOT_STATUS_CALM_POWER_MODE\x10\x80\x08\x12\x1f\n\x1aROBOT_STATUS_IS_ON_CHARGER\x10\x80 \x12\x1d\n\x18ROBOT_STATUS_IS_CHARGING\x10\x80@\x12!\n\x1bROBOT_STATUS_CLIFF_DETECTED\x10\x80\x80\x01\x12$\n\x1eROBOT_STATUS_ARE_WHEELS_MOVING\x10\x80\x80\x02\x12 \n\x1aROBOT_STATUS_IS_BEING_HELD\x10\x80\x80\x04\x12%\n\x1fROBOT_STATUS_IS_MOTION_DETECTED\x10\x80\x80\x08*\xbd\x01\n\x10\x46\x61\x63ialExpression\x12\x16\n\x12\x45XPRESSION_UNKNOWN\x10\x00\x12\x16\n\x12\x45XPRESSION_NEUTRAL\x10\x01\x12\x18\n\x14\x45XPRESSION_HAPPINESS\x10\x02\x12\x17\n\x13\x45XPRESSION_SURPRISE\x10\x03\x12\x14\n\x10\x45XPRESSION_ANGER\x10\x04\x12\x16\n\x12\x45XPRESSION_SADNESS\x10\x05\x12\x14\n\x10\x45XPRESSION_COUNT\x10\x05\x1a\x02\x10\x01*\xcc\x01\n\x14\x46\x61\x63\x65\x45nrollmentResult\x12\x0b\n\x07SUCCESS\x10\x00\x12\x12\n\x0eSAW_WRONG_FACE\x10\x01\x12\x16\n\x12SAW_MULTIPLE_FACES\x10\x02\x12\r\n\tTIMED_OUT\x10\x03\x12\x0f\n\x0bSAVE_FAILED\x10\x04\x12\x0e\n\nINCOMPLETE\x10\x05\x12\r\n\tCANCELLED\x10\x06\x12\x0f\n\x0bNAME_IN_USE\x10\x07\x12\x16\n\x12NAMED_STORAGE_FULL\x10\x08\x12\x13\n\x0fUNKNOWN_FAILURE\x10\t*l\n\x0f\x42\x65haviorResults\x12\x1a\n\x16\x42\x45HAVIOR_INVALID_STATE\x10\x00\x12\x1b\n\x17\x42\x45HAVIOR_COMPLETE_STATE\x10\x01\x12 \n\x1c\x42\x45HAVIOR_WONT_ACTIVATE_STATE\x10\x02*S\n\x12\x41\x63tionTagConstants\x12\x13\n\x0fINVALID_SDK_TAG\x10\x00\x12\x13\n\rFIRST_SDK_TAG\x10\x81\x89z\x12\x13\n\x0cLAST_SDK_TAG\x10\xc0\x8d\xb7\x01*\x9e\x01\n\rAlignmentType\x12\x1a\n\x16\x41LIGNMENT_TYPE_UNKNOWN\x10\x00\x12\x1e\n\x1a\x41LIGNMENT_TYPE_LIFT_FINGER\x10\x01\x12\x1d\n\x19\x41LIGNMENT_TYPE_LIFT_PLATE\x10\x02\x12\x17\n\x13\x41LIGNMENT_TYPE_BODY\x10\x03\x12\x19\n\x15\x41LIGNMENT_TYPE_CUSTOM\x10\x04*s\n\x0c\x42\x61tteryLevel\x12\x19\n\x15\x42\x41TTERY_LEVEL_UNKNOWN\x10\x00\x12\x15\n\x11\x42\x41TTERY_LEVEL_LOW\x10\x01\x12\x19\n\x15\x42\x41TTERY_LEVEL_NOMINAL\x10\x02\x12\x16\n\x12\x42\x41TTERY_LEVEL_FULL\x10\x03*\xcc\x01\n\x0e\x41udioConstants\x12\x18\n\x14\x41UDIO_CONSTANTS_NULL\x10\x00\x12\x1c\n\x18MIC_DETECTION_DIRECTIONS\x10\x0c\x12%\n SAMPLE_COUNTS_PER_ENGINE_MESSAGE\x10\xa0\x01\x12\"\n\x1dSAMPLE_COUNTS_PER_SDK_MESSAGE\x10\xc0\x0c\x12\x1b\n\x16MICROPHONE_SAMPLE_RATE\x10\x89z\x12\x1a\n\x15PROCESSED_SAMPLE_RATE\x10\x80}*\x85\x01\n\x13\x41udioProcessingMode\x12\x11\n\rAUDIO_UNKNOWN\x10\x00\x12\r\n\tAUDIO_OFF\x10\x01\x12\x13\n\x0f\x41UDIO_FAST_MODE\x10\x02\x12\x1a\n\x16\x41UDIO_DIRECTIONAL_MODE\x10\x03\x12\x1b\n\x17\x41UDIO_VOICE_DETECT_MODE\x10\x04*v\n\x11MasterVolumeLevel\x12\x0e\n\nVOLUME_LOW\x10\x00\x12\x15\n\x11VOLUME_MEDIUM_LOW\x10\x01\x12\x11\n\rVOLUME_MEDIUM\x10\x02\x12\x16\n\x12VOLUME_MEDIUM_HIGH\x10\x03\x12\x0f\n\x0bVOLUME_HIGH\x10\x04\x62\x06proto3') + serialized_pb=_b('\n$anki_vector/messaging/messages.proto\x12\x1e\x41nki.Vector.external_interface\x1a+anki_vector/messaging/response_status.proto\x1a&anki_vector/messaging/extensions.proto\"\x0f\n\rKeepAlivePing\" \n\x10\x41nimationTrigger\x12\x0c\n\x04name\x18\x01 \x01(\t\"\x19\n\tAnimation\x12\x0c\n\x04name\x18\x01 \x01(\t\"|\n\x12\x44riveWheelsRequest\x12\x17\n\x0fleft_wheel_mmps\x18\x01 \x01(\x02\x12\x18\n\x10right_wheel_mmps\x18\x02 \x01(\x02\x12\x18\n\x10left_wheel_mmps2\x18\x03 \x01(\x02\x12\x19\n\x11right_wheel_mmps2\x18\x04 \x01(\x02\"U\n\x13\x44riveWheelsResponse\x12>\n\x06status\x18\x01 \x01(\x0b\x32..Anki.Vector.external_interface.ResponseStatus\",\n\x0fMoveHeadRequest\x12\x19\n\x11speed_rad_per_sec\x18\x01 \x01(\x02\"R\n\x10MoveHeadResponse\x12>\n\x06status\x18\x01 \x01(\x0b\x32..Anki.Vector.external_interface.ResponseStatus\",\n\x0fMoveLiftRequest\x12\x19\n\x11speed_rad_per_sec\x18\x01 \x01(\x02\"R\n\x10MoveLiftResponse\x12>\n\x06status\x18\x01 \x01(\x0b\x32..Anki.Vector.external_interface.ResponseStatus\"\x16\n\x14StopAllMotorsRequest\"W\n\x15StopAllMotorsResponse\x12>\n\x06status\x18\x01 \x01(\x0b\x32..Anki.Vector.external_interface.ResponseStatus\"\xe1\x01\n\x1bPlayAnimationTriggerRequest\x12K\n\x11\x61nimation_trigger\x18\x01 \x01(\x0b\x32\x30.Anki.Vector.external_interface.AnimationTrigger\x12\r\n\x05loops\x18\x02 \x01(\r\x12\x15\n\ruse_lift_safe\x18\x03 \x01(\x08\x12\x19\n\x11ignore_body_track\x18\x04 \x01(\x08\x12\x19\n\x11ignore_head_track\x18\x05 \x01(\x08\x12\x19\n\x11ignore_lift_track\x18\x06 \x01(\x08\"\xb4\x01\n\x14PlayAnimationRequest\x12<\n\tanimation\x18\x01 \x01(\x0b\x32).Anki.Vector.external_interface.Animation\x12\r\n\x05loops\x18\x02 \x01(\r\x12\x19\n\x11ignore_body_track\x18\x03 \x01(\x08\x12\x19\n\x11ignore_head_track\x18\x04 \x01(\x08\x12\x19\n\x11ignore_lift_track\x18\x05 \x01(\x08\"\xd6\x01\n\x15PlayAnimationResponse\x12>\n\x06status\x18\x01 \x01(\x0b\x32..Anki.Vector.external_interface.ResponseStatus\x12?\n\x06result\x18\x02 \x01(\x0e\x32/.Anki.Vector.external_interface.BehaviorResults\x12<\n\tanimation\x18\x03 \x01(\x0b\x32).Anki.Vector.external_interface.Animation\"\x17\n\x15ListAnimationsRequest\"\x9c\x01\n\x16ListAnimationsResponse\x12>\n\x06status\x18\x01 \x01(\x0b\x32..Anki.Vector.external_interface.ResponseStatus\x12\x42\n\x0f\x61nimation_names\x18\x02 \x03(\x0b\x32).Anki.Vector.external_interface.Animation\"\x1e\n\x1cListAnimationTriggersRequest\"\xb2\x01\n\x1dListAnimationTriggersResponse\x12>\n\x06status\x18\x01 \x01(\x0b\x32..Anki.Vector.external_interface.ResponseStatus\x12Q\n\x17\x61nimation_trigger_names\x18\x02 \x03(\x0b\x32\x30.Anki.Vector.external_interface.AnimationTrigger\"_\n\x1a\x44isplayFaceImageRGBRequest\x12\x11\n\tface_data\x18\x01 \x01(\x0c\x12\x13\n\x0b\x64uration_ms\x18\x02 \x01(\r\x12\x19\n\x11interrupt_running\x18\x03 \x01(\x08\"]\n\x1b\x44isplayFaceImageRGBResponse\x12>\n\x06status\x18\x01 \x01(\x0b\x32..Anki.Vector.external_interface.ResponseStatus\"\x1b\n\x19MeetVictorFaceScanStarted\"\x1c\n\x1aMeetVictorFaceScanComplete\"\xbf\x02\n\x06Status\x12\x62\n\x1dmeet_victor_face_scan_started\x18\x02 \x01(\x0b\x32\x39.Anki.Vector.external_interface.MeetVictorFaceScanStartedH\x00\x12\x64\n\x1emeet_victor_face_scan_complete\x18\x03 \x01(\x0b\x32:.Anki.Vector.external_interface.MeetVictorFaceScanCompleteH\x00\x12\\\n\x19\x66\x61\x63\x65_enrollment_completed\x18\x04 \x01(\x0b\x32\x37.Anki.Vector.external_interface.FaceEnrollmentCompletedH\x00\x42\r\n\x0bstatus_type\"\xac\x01\n\x08WakeWord\x12H\n\x0fwake_word_begin\x18\x01 \x01(\x0b\x32-.Anki.Vector.external_interface.WakeWordBeginH\x00\x12\x44\n\rwake_word_end\x18\x02 \x01(\x0b\x32+.Anki.Vector.external_interface.WakeWordEndH\x00\x42\x10\n\x0ewake_word_type\"b\n\x11TimeStampedStatus\x12\x36\n\x06status\x18\x01 \x01(\x0b\x32&.Anki.Vector.external_interface.Status\x12\x15\n\rtimestamp_utc\x18\x02 \x01(\r\"p\n\nPoseStruct\x12\t\n\x01x\x18\x01 \x01(\x02\x12\t\n\x01y\x18\x02 \x01(\x02\x12\t\n\x01z\x18\x03 \x01(\x02\x12\n\n\x02q0\x18\x04 \x01(\x02\x12\n\n\x02q1\x18\x05 \x01(\x02\x12\n\n\x02q2\x18\x06 \x01(\x02\x12\n\n\x02q3\x18\x07 \x01(\x02\x12\x11\n\torigin_id\x18\x08 \x01(\r\",\n\tAccelData\x12\t\n\x01x\x18\x01 \x01(\x02\x12\t\n\x01y\x18\x02 \x01(\x02\x12\t\n\x01z\x18\x03 \x01(\x02\"+\n\x08GyroData\x12\t\n\x01x\x18\x01 \x01(\x02\x12\t\n\x01y\x18\x02 \x01(\x02\x12\t\n\x01z\x18\x03 \x01(\x02\"{\n\x08ProxData\x12\x13\n\x0b\x64istance_mm\x18\x01 \x01(\r\x12\x16\n\x0esignal_quality\x18\x02 \x01(\x02\x12\x14\n\x0cunobstructed\x18\x03 \x01(\x08\x12\x14\n\x0c\x66ound_object\x18\x04 \x01(\x08\x12\x16\n\x0eis_lift_in_fov\x18\x05 \x01(\x08\">\n\tTouchData\x12\x17\n\x0fraw_touch_value\x18\x01 \x01(\r\x12\x18\n\x10is_being_touched\x18\x02 \x01(\x08\"\x82\x05\n\nRobotState\x12\x38\n\x04pose\x18\x01 \x01(\x0b\x32*.Anki.Vector.external_interface.PoseStruct\x12\x16\n\x0epose_angle_rad\x18\x02 \x01(\x02\x12\x16\n\x0epose_pitch_rad\x18\x03 \x01(\x02\x12\x1d\n\x15left_wheel_speed_mmps\x18\x04 \x01(\x02\x12\x1e\n\x16right_wheel_speed_mmps\x18\x05 \x01(\x02\x12\x16\n\x0ehead_angle_rad\x18\x06 \x01(\x02\x12\x16\n\x0elift_height_mm\x18\x07 \x01(\x02\x12\x38\n\x05\x61\x63\x63\x65l\x18\x08 \x01(\x0b\x32).Anki.Vector.external_interface.AccelData\x12\x36\n\x04gyro\x18\t \x01(\x0b\x32(.Anki.Vector.external_interface.GyroData\x12\x1a\n\x12\x63\x61rrying_object_id\x18\n \x01(\x05\x12!\n\x19\x63\x61rrying_object_on_top_id\x18\x0b \x01(\x05\x12\x1f\n\x17head_tracking_object_id\x18\x0c \x01(\x05\x12\x1e\n\x16localized_to_object_id\x18\r \x01(\x05\x12\x1d\n\x15last_image_time_stamp\x18\x0e \x01(\r\x12\x0e\n\x06status\x18\x0f \x01(\r\x12;\n\tprox_data\x18\x10 \x01(\x0b\x32(.Anki.Vector.external_interface.ProxData\x12=\n\ntouch_data\x18\x11 \x01(\x0b\x32).Anki.Vector.external_interface.TouchData\"!\n\tCladPoint\x12\t\n\x01x\x18\x01 \x01(\x02\x12\t\n\x01y\x18\x02 \x01(\x02\"Q\n\x08\x43ladRect\x12\x12\n\nx_top_left\x18\x01 \x01(\x02\x12\x12\n\ny_top_left\x18\x02 \x01(\x02\x12\r\n\x05width\x18\x03 \x01(\x02\x12\x0e\n\x06height\x18\x04 \x01(\x02\"\x93\x03\n\x13RobotObservedMotion\x12\x11\n\ttimestamp\x18\x01 \x01(\r\x12\x10\n\x08img_area\x18\x02 \x01(\x02\x12\r\n\x05img_x\x18\x03 \x01(\x05\x12\r\n\x05img_y\x18\x04 \x01(\x05\x12\x13\n\x0bground_area\x18\x05 \x01(\x02\x12\x10\n\x08ground_x\x18\x06 \x01(\x05\x12\x10\n\x08ground_y\x18\x07 \x01(\x05\x12\x14\n\x0ctop_img_area\x18\x08 \x01(\x02\x12\x11\n\ttop_img_x\x18\t \x01(\x05\x12\x11\n\ttop_img_y\x18\n \x01(\x05\x12\x17\n\x0f\x62ottom_img_area\x18\x0b \x01(\x02\x12\x14\n\x0c\x62ottom_img_x\x18\x0c \x01(\x05\x12\x14\n\x0c\x62ottom_img_y\x18\r \x01(\x05\x12\x15\n\rleft_img_area\x18\x0e \x01(\x02\x12\x12\n\nleft_img_x\x18\x0f \x01(\x05\x12\x12\n\nleft_img_y\x18\x10 \x01(\x05\x12\x16\n\x0eright_img_area\x18\x11 \x01(\x02\x12\x13\n\x0bright_img_x\x18\x12 \x01(\x05\x12\x13\n\x0bright_img_y\x18\x13 \x01(\x05\"8\n\x17RobotErasedEnrolledFace\x12\x0f\n\x07\x66\x61\x63\x65_id\x18\x01 \x01(\x05\x12\x0c\n\x04name\x18\x02 \x01(\t\"\xc5\x01\n\x12UnexpectedMovement\x12\x11\n\ttimestamp\x18\x01 \x01(\r\x12M\n\rmovement_type\x18\x02 \x01(\x0e\x32\x36.Anki.Vector.external_interface.UnexpectedMovementType\x12M\n\rmovement_side\x18\x03 \x01(\x0e\x32\x36.Anki.Vector.external_interface.UnexpectedMovementSide\"\x8a\x04\n\x11RobotObservedFace\x12\x0f\n\x07\x66\x61\x63\x65_id\x18\x01 \x01(\x05\x12\x11\n\ttimestamp\x18\x02 \x01(\r\x12\x38\n\x04pose\x18\x03 \x01(\x0b\x32*.Anki.Vector.external_interface.PoseStruct\x12:\n\x08img_rect\x18\x04 \x01(\x0b\x32(.Anki.Vector.external_interface.CladRect\x12\x0c\n\x04name\x18\x05 \x01(\t\x12\x44\n\nexpression\x18\x06 \x01(\x0e\x32\x30.Anki.Vector.external_interface.FacialExpression\x12\x19\n\x11\x65xpression_values\x18\x07 \x03(\r\x12;\n\x08left_eye\x18\x08 \x03(\x0b\x32).Anki.Vector.external_interface.CladPoint\x12<\n\tright_eye\x18\t \x03(\x0b\x32).Anki.Vector.external_interface.CladPoint\x12\x37\n\x04nose\x18\n \x03(\x0b\x32).Anki.Vector.external_interface.CladPoint\x12\x38\n\x05mouth\x18\x0b \x03(\x0b\x32).Anki.Vector.external_interface.CladPoint\"<\n\x1aRobotChangedObservedFaceID\x12\x0e\n\x06old_id\x18\x01 \x01(\x05\x12\x0e\n\x06new_id\x18\x02 \x01(\x05\"~\n\x17\x46\x61\x63\x65\x45nrollmentCompleted\x12\x44\n\x06result\x18\x01 \x01(\x0e\x32\x34.Anki.Vector.external_interface.FaceEnrollmentResult\x12\x0f\n\x07\x66\x61\x63\x65_id\x18\x02 \x01(\x05\x12\x0c\n\x04name\x18\x03 \x01(\t\"\x1d\n\x1b\x43\x61ncelFaceEnrollmentRequest\"^\n\x1c\x43\x61ncelFaceEnrollmentResponse\x12>\n\x06status\x18\x01 \x01(\x0b\x32..Anki.Vector.external_interface.ResponseStatus\"\x1d\n\x1bRequestEnrolledNamesRequest\"\xc2\x01\n\x0fLoadedKnownFace\x12$\n\x1cseconds_since_first_enrolled\x18\x01 \x01(\x03\x12\"\n\x1aseconds_since_last_updated\x18\x02 \x01(\x03\x12\x1f\n\x17seconds_since_last_seen\x18\x03 \x01(\x03\x12%\n\x1dlast_seen_seconds_since_epoch\x18\x04 \x01(\x03\x12\x0f\n\x07\x66\x61\x63\x65_id\x18\x05 \x01(\x05\x12\x0c\n\x04name\x18\x06 \x01(\t\"9\n\x18RobotRenamedEnrolledFace\x12\x0f\n\x07\x66\x61\x63\x65_id\x18\x01 \x01(\x05\x12\x0c\n\x04name\x18\x02 \x01(\t\"\x9e\x01\n\x1cRequestEnrolledNamesResponse\x12>\n\x06status\x18\x01 \x01(\x0b\x32..Anki.Vector.external_interface.ResponseStatus\x12>\n\x05\x66\x61\x63\x65s\x18\x02 \x03(\x0b\x32/.Anki.Vector.external_interface.LoadedKnownFace\"T\n\x1dUpdateEnrolledFaceByIDRequest\x12\x0f\n\x07\x66\x61\x63\x65_id\x18\x01 \x01(\x05\x12\x10\n\x08old_name\x18\x02 \x01(\t\x12\x10\n\x08new_name\x18\x03 \x01(\t\"`\n\x1eUpdateEnrolledFaceByIDResponse\x12>\n\x06status\x18\x01 \x01(\x0b\x32..Anki.Vector.external_interface.ResponseStatus\"/\n\x1c\x45raseEnrolledFaceByIDRequest\x12\x0f\n\x07\x66\x61\x63\x65_id\x18\x01 \x01(\x05\"_\n\x1d\x45raseEnrolledFaceByIDResponse\x12>\n\x06status\x18\x01 \x01(\x0b\x32..Anki.Vector.external_interface.ResponseStatus\"\x1e\n\x1c\x45raseAllEnrolledFacesRequest\"_\n\x1d\x45raseAllEnrolledFacesResponse\x12>\n\x06status\x18\x01 \x01(\x0b\x32..Anki.Vector.external_interface.ResponseStatus\"\x88\x01\n\x16SetFaceToEnrollRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x13\n\x0bobserved_id\x18\x02 \x01(\x05\x12\x0f\n\x07save_id\x18\x03 \x01(\x05\x12\x15\n\rsave_to_robot\x18\x04 \x01(\x08\x12\x10\n\x08say_name\x18\x05 \x01(\x08\x12\x11\n\tuse_music\x18\x06 \x01(\x08\"Y\n\x17SetFaceToEnrollResponse\x12>\n\x06status\x18\x01 \x01(\x0b\x32..Anki.Vector.external_interface.ResponseStatus\"\x13\n\x11\x45nrollFaceRequest\"\x95\x01\n\x12\x45nrollFaceResponse\x12>\n\x06status\x18\x01 \x01(\x0b\x32..Anki.Vector.external_interface.ResponseStatus\x12?\n\x06result\x18\x02 \x01(\x0e\x32/.Anki.Vector.external_interface.BehaviorResults\"\x18\n\x16\x44riveOffChargerRequest\"\x9a\x01\n\x17\x44riveOffChargerResponse\x12>\n\x06status\x18\x01 \x01(\x0b\x32..Anki.Vector.external_interface.ResponseStatus\x12?\n\x06result\x18\x02 \x01(\x0e\x32/.Anki.Vector.external_interface.BehaviorResults\"\x17\n\x15\x44riveOnChargerRequest\"\x99\x01\n\x16\x44riveOnChargerResponse\x12>\n\x06status\x18\x01 \x01(\x0b\x32..Anki.Vector.external_interface.ResponseStatus\x12?\n\x06result\x18\x02 \x01(\x0e\x32/.Anki.Vector.external_interface.BehaviorResults\"\x12\n\x10\x46indFacesRequest\"\x94\x01\n\x11\x46indFacesResponse\x12>\n\x06status\x18\x01 \x01(\x0b\x32..Anki.Vector.external_interface.ResponseStatus\x12?\n\x06result\x18\x02 \x01(\x0e\x32/.Anki.Vector.external_interface.BehaviorResults\"\x1a\n\x18LookAroundInPlaceRequest\"\x9c\x01\n\x19LookAroundInPlaceResponse\x12>\n\x06status\x18\x01 \x01(\x0b\x32..Anki.Vector.external_interface.ResponseStatus\x12?\n\x06result\x18\x02 \x01(\x0e\x32/.Anki.Vector.external_interface.BehaviorResults\"\x12\n\x10RollBlockRequest\"\x94\x01\n\x11RollBlockResponse\x12>\n\x06status\x18\x01 \x01(\x0b\x32..Anki.Vector.external_interface.ResponseStatus\x12?\n\x06result\x18\x02 \x01(\x0e\x32/.Anki.Vector.external_interface.BehaviorResults\"\x0f\n\rWakeWordBegin\"8\n\x0bWakeWordEnd\x12\x14\n\x0cintent_heard\x18\x01 \x01(\x08\x12\x13\n\x0bintent_json\x18\x02 \x01(\t\"2\n\nUserIntent\x12\x11\n\tintent_id\x18\x01 \x01(\r\x12\x11\n\tjson_data\x18\x02 \x01(\t\"n\n\tPhotoInfo\x12\x10\n\x08photo_id\x18\x01 \x01(\r\x12\x15\n\rtimestamp_utc\x18\x02 \x01(\r\x12\x1b\n\x13photo_copied_to_app\x18\x03 \x01(\x08\x12\x1b\n\x13thumb_copied_to_app\x18\x04 \x01(\x08\"\x13\n\x11PhotosInfoRequest\"\x94\x01\n\x12PhotosInfoResponse\x12>\n\x06status\x18\x01 \x01(\x0b\x32..Anki.Vector.external_interface.ResponseStatus\x12>\n\x0bphoto_infos\x18\x02 \x03(\x0b\x32).Anki.Vector.external_interface.PhotoInfo\" \n\x0cPhotoRequest\x12\x10\n\x08photo_id\x18\x01 \x01(\r\"6\n\x10PhotoPathMessage\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\x11\n\tfull_path\x18\x02 \x01(\t\"o\n\rPhotoResponse\x12>\n\x06status\x18\x01 \x01(\x0b\x32..Anki.Vector.external_interface.ResponseStatus\x12\x0f\n\x07success\x18\x02 \x01(\x08\x12\r\n\x05image\x18\x03 \x01(\x0c\"$\n\x10ThumbnailRequest\x12\x10\n\x08photo_id\x18\x01 \x01(\r\":\n\x14ThumbnailPathMessage\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\x11\n\tfull_path\x18\x02 \x01(\t\"s\n\x11ThumbnailResponse\x12>\n\x06status\x18\x01 \x01(\x0b\x32..Anki.Vector.external_interface.ResponseStatus\x12\x0f\n\x07success\x18\x02 \x01(\x08\x12\r\n\x05image\x18\x03 \x01(\x0c\"&\n\x12\x44\x65letePhotoRequest\x12\x10\n\x08photo_id\x18\x01 \x01(\r\"f\n\x13\x44\x65letePhotoResponse\x12>\n\x06status\x18\x01 \x01(\x0b\x32..Anki.Vector.external_interface.ResponseStatus\x12\x0f\n\x07success\x18\x02 \x01(\x08\"\x1e\n\nPhotoTaken\x12\x10\n\x08photo_id\x18\x01 \x01(\r\"\xc1\x02\n\x11PathMotionProfile\x12\x12\n\nspeed_mmps\x18\x01 \x01(\x02\x12\x13\n\x0b\x61\x63\x63\x65l_mmps2\x18\x02 \x01(\x02\x12\x13\n\x0b\x64\x65\x63\x65l_mmps2\x18\x03 \x01(\x02\x12$\n\x1cpoint_turn_speed_rad_per_sec\x18\x04 \x01(\x02\x12%\n\x1dpoint_turn_accel_rad_per_sec2\x18\x05 \x01(\x02\x12%\n\x1dpoint_turn_decel_rad_per_sec2\x18\x06 \x01(\x02\x12\x17\n\x0f\x64ock_speed_mmps\x18\x07 \x01(\x02\x12\x18\n\x10\x64ock_accel_mmps2\x18\x08 \x01(\x02\x12\x18\n\x10\x64ock_decel_mmps2\x18\t \x01(\x02\x12\x1a\n\x12reverse_speed_mmps\x18\n \x01(\x02\x12\x11\n\tis_custom\x18\x0b \x01(\x08\"\xf8\x0c\n\x0c\x41\x63tionResult\x12K\n\x04\x63ode\x18\x01 \x01(\x0e\x32=.Anki.Vector.external_interface.ActionResult.ActionResultCode\"\x9a\x0c\n\x10\x41\x63tionResultCode\x12\x19\n\x15\x41\x43TION_RESULT_SUCCESS\x10\x00\x12\x1c\n\x15\x41\x43TION_RESULT_RUNNING\x10\x80\x80\x80\x08\x12,\n%ACTION_RESULT_CANCELLED_WHILE_RUNNING\x10\x80\x80\x80\x10\x12\x12\n\x0bNOT_STARTED\x10\x81\x80\x80\x10\x12\x0c\n\x05\x41\x42ORT\x10\x80\x80\x80\x18\x12\x13\n\x0c\x41NIM_ABORTED\x10\x81\x80\x80\x18\x12\x11\n\nBAD_MARKER\x10\x82\x80\x80\x18\x12\x16\n\x0f\x42\x41\x44_MESSAGE_TAG\x10\x83\x80\x80\x18\x12\x11\n\nBAD_OBJECT\x10\x84\x80\x80\x18\x12\x0f\n\x08\x42\x41\x44_POSE\x10\x85\x80\x80\x18\x12\x0e\n\x07\x42\x41\x44_TAG\x10\x86\x80\x80\x18\x12\x1e\n\x17\x43HARGER_UNPLUGGED_ABORT\x10\x87\x80\x80\x18\x12!\n\x1a\x43LIFF_ALIGN_FAILED_TIMEOUT\x10\x88\x80\x80\x18\x12$\n\x1d\x43LIFF_ALIGN_FAILED_NO_TURNING\x10\x89\x80\x80\x18\x12&\n\x1f\x43LIFF_ALIGN_FAILED_OVER_TURNING\x10\x8a\x80\x80\x18\x12\"\n\x1b\x43LIFF_ALIGN_FAILED_NO_WHITE\x10\x8b\x80\x80\x18\x12!\n\x1a\x43LIFF_ALIGN_FAILED_STOPPED\x10\x8c\x80\x80\x18\x12!\n\x1a\x46\x41ILED_SETTING_CALIBRATION\x10\x8d\x80\x80\x18\x12(\n!FOLLOWING_PATH_BUT_NOT_TRAVERSING\x10\x8e\x80\x80\x18\x12\x12\n\x0bINTERRUPTED\x10\x8f\x80\x80\x18\x12\x1f\n\x18INVALID_OFF_TREADS_STATE\x10\x90\x80\x80\x18\x12\x19\n\x12MISMATCHED_UP_AXIS\x10\x91\x80\x80\x18\x12\x13\n\x0cNO_ANIM_NAME\x10\x92\x80\x80\x18\x12\x16\n\x0fNO_DISTANCE_SET\x10\x93\x80\x80\x18\x12\x0e\n\x07NO_FACE\x10\x94\x80\x80\x18\x12\x12\n\x0bNO_GOAL_SET\x10\x95\x80\x80\x18\x12\x19\n\x12NO_PREACTION_POSES\x10\x96\x80\x80\x18\x12 \n\x19NOT_CARRYING_OBJECT_ABORT\x10\x97\x80\x80\x18\x12\x1b\n\x14NOT_ON_CHARGER_ABORT\x10\x98\x80\x80\x18\x12\x15\n\x0eNULL_SUBACTION\x10\x99\x80\x80\x18\x12!\n\x1aPATH_PLANNING_FAILED_ABORT\x10\x9a\x80\x80\x18\x12(\n!PICKUP_OBJECT_UNEXPECTEDLY_MOVING\x10\x9b\x80\x80\x18\x12#\n\x1cSEND_MESSAGE_TO_ROBOT_FAILED\x10\x9c\x80\x80\x18\x12\x1c\n\x15STILL_CARRYING_OBJECT\x10\x9d\x80\x80\x18\x12\x0e\n\x07TIMEOUT\x10\x9e\x80\x80\x18\x12\x14\n\rTRACKS_LOCKED\x10\x9f\x80\x80\x18\x12\x1d\n\x16UNEXPECTED_DOCK_ACTION\x10\xa0\x80\x80\x18\x12\x18\n\x11UNKNOWN_TOOL_CODE\x10\xa1\x80\x80\x18\x12\x1c\n\x15UPDATE_DERIVED_FAILED\x10\xa2\x80\x80\x18\x12 \n\x19VISUAL_OBSERVATION_FAILED\x10\xa3\x80\x80\x18\x12 \n\x19SHOULDNT_DRIVE_ON_CHARGER\x10\xa4\x80\x80\x18\x12\x0c\n\x05RETRY\x10\x80\x80\x80 \x12#\n\x1c\x44ID_NOT_REACH_PREACTION_POSE\x10\x81\x80\x80 \x12\x1d\n\x16\x46\x41ILED_TRAVERSING_PATH\x10\x82\x80\x80 \x12!\n\x1aLAST_PICK_AND_PLACE_FAILED\x10\x83\x80\x80 \x12$\n\x1dMOTOR_STOPPED_MAKING_PROGRESS\x10\x84\x80\x80 \x12 \n\x19NOT_CARRYING_OBJECT_RETRY\x10\x85\x80\x80 \x12\x1b\n\x14NOT_ON_CHARGER_RETRY\x10\x86\x80\x80 \x12!\n\x1aPATH_PLANNING_FAILED_RETRY\x10\x87\x80\x80 \x12\x1e\n\x17PLACEMENT_GOAL_NOT_FREE\x10\x88\x80\x80 \x12,\n%PICKUP_OBJECT_UNEXPECTEDLY_NOT_MOVING\x10\x89\x80\x80 \x12\x17\n\x10STILL_ON_CHARGER\x10\x8a\x80\x80 \x12\x1d\n\x16UNEXPECTED_PITCH_ANGLE\x10\x8b\x80\x80 \",\n\x1a\x43\x61ncelActionByIdTagRequest\x12\x0e\n\x06id_tag\x18\x01 \x01(\r\"]\n\x1b\x43\x61ncelActionByIdTagResponse\x12>\n\x06status\x18\x01 \x01(\x0b\x32..Anki.Vector.external_interface.ResponseStatus\"\x17\n\x15\x43\x61ncelBehaviorRequest\"X\n\x16\x43\x61ncelBehaviorResponse\x12>\n\x06status\x18\x01 \x01(\x0b\x32..Anki.Vector.external_interface.ResponseStatus\"\xa7\x01\n\x0fGoToPoseRequest\x12\x0c\n\x04x_mm\x18\x01 \x01(\x02\x12\x0c\n\x04y_mm\x18\x02 \x01(\x02\x12\x0b\n\x03rad\x18\x03 \x01(\x02\x12\x46\n\x0bmotion_prof\x18\x04 \x01(\x0b\x32\x31.Anki.Vector.external_interface.PathMotionProfile\x12\x0e\n\x06id_tag\x18\x05 \x01(\x05\x12\x13\n\x0bnum_retries\x18\x06 \x01(\x05\"\x90\x01\n\x10GoToPoseResponse\x12>\n\x06status\x18\x01 \x01(\x0b\x32..Anki.Vector.external_interface.ResponseStatus\x12<\n\x06result\x18\x02 \x01(\x0b\x32,.Anki.Vector.external_interface.ActionResult\"\xd0\x02\n\x13\x44ockWithCubeRequest\x12\x11\n\tobject_id\x18\x01 \x01(\x05\x12\x1f\n\x17\x64istance_from_marker_mm\x18\x02 \x01(\x02\x12\x1a\n\x12\x61pproach_angle_rad\x18\x03 \x01(\x02\x12\x45\n\x0e\x61lignment_type\x18\x04 \x01(\x0e\x32-.Anki.Vector.external_interface.AlignmentType\x12\x1a\n\x12use_approach_angle\x18\x05 \x01(\x08\x12\x19\n\x11use_pre_dock_pose\x18\x06 \x01(\x08\x12\x46\n\x0bmotion_prof\x18\x07 \x01(\x0b\x32\x31.Anki.Vector.external_interface.PathMotionProfile\x12\x0e\n\x06id_tag\x18\x08 \x01(\x05\x12\x13\n\x0bnum_retries\x18\t \x01(\x05\"\x94\x01\n\x14\x44ockWithCubeResponse\x12>\n\x06status\x18\x01 \x01(\x0b\x32..Anki.Vector.external_interface.ResponseStatus\x12<\n\x06result\x18\x02 \x01(\x0b\x32,.Anki.Vector.external_interface.ActionResult\"\x7f\n\x14\x44riveStraightRequest\x12\x12\n\nspeed_mmps\x18\x01 \x01(\x02\x12\x0f\n\x07\x64ist_mm\x18\x02 \x01(\x02\x12\x1d\n\x15should_play_animation\x18\x03 \x01(\x08\x12\x0e\n\x06id_tag\x18\x04 \x01(\x05\x12\x13\n\x0bnum_retries\x18\x05 \x01(\x05\"\x95\x01\n\x15\x44riveStraightResponse\x12>\n\x06status\x18\x01 \x01(\x0b\x32..Anki.Vector.external_interface.ResponseStatus\x12<\n\x06result\x18\x02 \x01(\x0b\x32,.Anki.Vector.external_interface.ActionResult\"\xa9\x01\n\x12TurnInPlaceRequest\x12\x11\n\tangle_rad\x18\x01 \x01(\x02\x12\x19\n\x11speed_rad_per_sec\x18\x02 \x01(\x02\x12\x1a\n\x12\x61\x63\x63\x65l_rad_per_sec2\x18\x03 \x01(\x02\x12\x0f\n\x07tol_rad\x18\x04 \x01(\x02\x12\x13\n\x0bis_absolute\x18\x05 \x01(\r\x12\x0e\n\x06id_tag\x18\x06 \x01(\x05\x12\x13\n\x0bnum_retries\x18\x07 \x01(\x05\"\x93\x01\n\x13TurnInPlaceResponse\x12>\n\x06status\x18\x01 \x01(\x0b\x32..Anki.Vector.external_interface.ResponseStatus\x12<\n\x06result\x18\x02 \x01(\x0b\x32,.Anki.Vector.external_interface.ActionResult\"\x9e\x01\n\x13SetHeadAngleRequest\x12\x11\n\tangle_rad\x18\x01 \x01(\x02\x12\x1d\n\x15max_speed_rad_per_sec\x18\x02 \x01(\x02\x12\x1a\n\x12\x61\x63\x63\x65l_rad_per_sec2\x18\x03 \x01(\x02\x12\x14\n\x0c\x64uration_sec\x18\x04 \x01(\x02\x12\x0e\n\x06id_tag\x18\x05 \x01(\x05\x12\x13\n\x0bnum_retries\x18\x06 \x01(\x05\"\x94\x01\n\x14SetHeadAngleResponse\x12>\n\x06status\x18\x01 \x01(\x0b\x32..Anki.Vector.external_interface.ResponseStatus\x12<\n\x06result\x18\x02 \x01(\x0b\x32,.Anki.Vector.external_interface.ActionResult\"\x9f\x01\n\x14SetLiftHeightRequest\x12\x11\n\theight_mm\x18\x01 \x01(\x02\x12\x1d\n\x15max_speed_rad_per_sec\x18\x02 \x01(\x02\x12\x1a\n\x12\x61\x63\x63\x65l_rad_per_sec2\x18\x03 \x01(\x02\x12\x14\n\x0c\x64uration_sec\x18\x04 \x01(\x02\x12\x0e\n\x06id_tag\x18\x05 \x01(\x05\x12\x13\n\x0bnum_retries\x18\x06 \x01(\x05\"\x95\x01\n\x15SetLiftHeightResponse\x12>\n\x06status\x18\x01 \x01(\x0b\x32..Anki.Vector.external_interface.ResponseStatus\x12<\n\x06result\x18\x02 \x01(\x0b\x32,.Anki.Vector.external_interface.ActionResult\"j\n\x16TurnTowardsFaceRequest\x12\x0f\n\x07\x66\x61\x63\x65_id\x18\x01 \x01(\x05\x12\x1a\n\x12max_turn_angle_rad\x18\x02 \x01(\x02\x12\x0e\n\x06id_tag\x18\x03 \x01(\x05\x12\x13\n\x0bnum_retries\x18\x04 \x01(\x05\"\x97\x01\n\x17TurnTowardsFaceResponse\x12>\n\x06status\x18\x01 \x01(\x0b\x32..Anki.Vector.external_interface.ResponseStatus\x12<\n\x06result\x18\x02 \x01(\x0b\x32,.Anki.Vector.external_interface.ActionResult\"\xd6\x01\n\x11GoToObjectRequest\x12\x11\n\tobject_id\x18\x01 \x01(\x05\x12\x46\n\x0bmotion_prof\x18\x02 \x01(\x0b\x32\x31.Anki.Vector.external_interface.PathMotionProfile\x12&\n\x1e\x64istance_from_object_origin_mm\x18\x03 \x01(\x02\x12\x19\n\x11use_pre_dock_pose\x18\x04 \x01(\x08\x12\x0e\n\x06id_tag\x18\x05 \x01(\x05\x12\x13\n\x0bnum_retries\x18\x06 \x01(\x05\"\x92\x01\n\x12GoToObjectResponse\x12>\n\x06status\x18\x01 \x01(\x0b\x32..Anki.Vector.external_interface.ResponseStatus\x12<\n\x06result\x18\x02 \x01(\x0b\x32,.Anki.Vector.external_interface.ActionResult\"\xe6\x01\n\x11RollObjectRequest\x12\x11\n\tobject_id\x18\x01 \x01(\x05\x12\x46\n\x0bmotion_prof\x18\x02 \x01(\x0b\x32\x31.Anki.Vector.external_interface.PathMotionProfile\x12\x1a\n\x12\x61pproach_angle_rad\x18\x03 \x01(\x02\x12\x1a\n\x12use_approach_angle\x18\x04 \x01(\x08\x12\x19\n\x11use_pre_dock_pose\x18\x05 \x01(\x08\x12\x0e\n\x06id_tag\x18\x06 \x01(\x05\x12\x13\n\x0bnum_retries\x18\x07 \x01(\x05\"\x92\x01\n\x12RollObjectResponse\x12>\n\x06status\x18\x01 \x01(\x0b\x32..Anki.Vector.external_interface.ResponseStatus\x12<\n\x06result\x18\x02 \x01(\x0b\x32,.Anki.Vector.external_interface.ActionResult\"\xe7\x01\n\x12PopAWheelieRequest\x12\x11\n\tobject_id\x18\x01 \x01(\x05\x12\x46\n\x0bmotion_prof\x18\x02 \x01(\x0b\x32\x31.Anki.Vector.external_interface.PathMotionProfile\x12\x1a\n\x12\x61pproach_angle_rad\x18\x03 \x01(\x02\x12\x1a\n\x12use_approach_angle\x18\x04 \x01(\x08\x12\x19\n\x11use_pre_dock_pose\x18\x05 \x01(\x08\x12\x0e\n\x06id_tag\x18\x06 \x01(\x05\x12\x13\n\x0bnum_retries\x18\x07 \x01(\x05\"\x93\x01\n\x13PopAWheelieResponse\x12>\n\x06status\x18\x01 \x01(\x0b\x32..Anki.Vector.external_interface.ResponseStatus\x12<\n\x06result\x18\x02 \x01(\x0b\x32,.Anki.Vector.external_interface.ActionResult\"\xe8\x01\n\x13PickupObjectRequest\x12\x11\n\tobject_id\x18\x01 \x01(\x05\x12\x46\n\x0bmotion_prof\x18\x02 \x01(\x0b\x32\x31.Anki.Vector.external_interface.PathMotionProfile\x12\x1a\n\x12\x61pproach_angle_rad\x18\x03 \x01(\x02\x12\x1a\n\x12use_approach_angle\x18\x04 \x01(\x08\x12\x19\n\x11use_pre_dock_pose\x18\x05 \x01(\x08\x12\x0e\n\x06id_tag\x18\x06 \x01(\x05\x12\x13\n\x0bnum_retries\x18\x07 \x01(\x05\"\x94\x01\n\x14PickupObjectResponse\x12>\n\x06status\x18\x01 \x01(\x0b\x32..Anki.Vector.external_interface.ResponseStatus\x12<\n\x06result\x18\x02 \x01(\x0b\x32,.Anki.Vector.external_interface.ActionResult\"E\n\x1ePlaceObjectOnGroundHereRequest\x12\x0e\n\x06id_tag\x18\x01 \x01(\x05\x12\x13\n\x0bnum_retries\x18\x02 \x01(\x05\"\x9f\x01\n\x1fPlaceObjectOnGroundHereResponse\x12>\n\x06status\x18\x01 \x01(\x0b\x32..Anki.Vector.external_interface.ResponseStatus\x12<\n\x06result\x18\x02 \x01(\x0b\x32,.Anki.Vector.external_interface.ActionResult\"\x15\n\x13\x42\x61tteryStateRequest\"\xc9\x02\n\x14\x42\x61tteryStateResponse\x12>\n\x06status\x18\x01 \x01(\x0b\x32..Anki.Vector.external_interface.ResponseStatus\x12\x43\n\rbattery_level\x18\x02 \x01(\x0e\x32,.Anki.Vector.external_interface.BatteryLevel\x12\x15\n\rbattery_volts\x18\x03 \x01(\x02\x12\x13\n\x0bis_charging\x18\x04 \x01(\x08\x12\x1e\n\x16is_on_charger_platform\x18\x05 \x01(\x08\x12\x1d\n\x15suggested_charger_sec\x18\x06 \x01(\x02\x12\x41\n\x0c\x63ube_battery\x18\x07 \x01(\x0b\x32+.Anki.Vector.external_interface.CubeBattery\"\xd3\x01\n\x0b\x43ubeBattery\x12K\n\x05level\x18\x01 \x01(\x0e\x32<.Anki.Vector.external_interface.CubeBattery.CubeBatteryLevel\x12\x12\n\nfactory_id\x18\x02 \x01(\t\x12\x15\n\rbattery_volts\x18\x03 \x01(\x02\x12#\n\x1btime_since_last_reading_sec\x18\x04 \x01(\x02\"\'\n\x10\x43ubeBatteryLevel\x12\x07\n\x03Low\x10\x00\x12\n\n\x06Normal\x10\x01\"\x15\n\x13VersionStateRequest\"\x83\x01\n\x14VersionStateResponse\x12>\n\x06status\x18\x01 \x01(\x0b\x32..Anki.Vector.external_interface.ResponseStatus\x12\x12\n\nos_version\x18\x02 \x01(\t\x12\x17\n\x0f\x65ngine_build_id\x18\x03 \x01(\t\"g\n\x0eSayTextRequest\x12\x0c\n\x04text\x18\x01 \x01(\t\x12\x18\n\x10use_vector_voice\x18\x02 \x01(\x08\x12\x17\n\x0f\x64uration_scalar\x18\x03 \x01(\x02\x12\x14\n\x0cpitch_scalar\x18\x04 \x01(\x02\"\xf5\x01\n\x0fSayTextResponse\x12>\n\x06status\x18\x01 \x01(\x0b\x32..Anki.Vector.external_interface.ResponseStatus\x12M\n\x05state\x18\x02 \x01(\x0e\x32>.Anki.Vector.external_interface.SayTextResponse.UtteranceState\"S\n\x0eUtteranceState\x12\x0b\n\x07INVALID\x10\x00\x12\x0e\n\nGENERATING\x10\x01\x12\t\n\x05READY\x10\x02\x12\x0b\n\x07PLAYING\x10\x03\x12\x0c\n\x08\x46INISHED\x10\x04\"\x9b\x01\n\x0fStimulationInfo\x12\x16\n\x0e\x65motion_events\x18\x01 \x03(\t\x12\r\n\x05value\x18\x02 \x01(\x02\x12\x10\n\x08velocity\x18\x03 \x01(\x02\x12\r\n\x05\x61\x63\x63\x65l\x18\x04 \x01(\x02\x12\x1a\n\x12value_before_event\x18\x05 \x01(\x02\x12\x11\n\tmin_value\x18\x06 \x01(\x02\x12\x11\n\tmax_value\x18\x07 \x01(\x02\"Y\n\x14\x41udioSendModeRequest\x12\x41\n\x04mode\x18\x01 \x01(\x0e\x32\x33.Anki.Vector.external_interface.AudioProcessingMode\"Y\n\x14\x41udioSendModeChanged\x12\x41\n\x04mode\x18\x01 \x01(\x0e\x32\x33.Anki.Vector.external_interface.AudioProcessingMode\"\xe8\x01\n\nAudioChunk\x12\x18\n\x10robot_time_stamp\x18\x01 \x01(\r\x12\x10\n\x08group_id\x18\x02 \x01(\r\x12\x10\n\x08\x63hunk_id\x18\x03 \x01(\r\x12\x19\n\x11\x61udio_chunk_count\x18\x04 \x01(\r\x12\x14\n\x0csignal_power\x18\x05 \x01(\x0c\x12\x1b\n\x13\x64irection_strengths\x18\x06 \x01(\x0c\x12\x18\n\x10source_direction\x18\x07 \x01(\r\x12\x19\n\x11source_confidence\x18\x08 \x01(\r\x12\x19\n\x11noise_floor_power\x18\t \x01(\r\"\x12\n\x10\x41udioFeedRequest\"\xc8\x01\n\x11\x41udioFeedResponse\x12\x18\n\x10robot_time_stamp\x18\x01 \x01(\r\x12\x10\n\x08group_id\x18\x02 \x01(\r\x12\x14\n\x0csignal_power\x18\x03 \x01(\x0c\x12\x1b\n\x13\x64irection_strengths\x18\x04 \x01(\x0c\x12\x18\n\x10source_direction\x18\x05 \x01(\r\x12\x19\n\x11source_confidence\x18\x06 \x01(\r\x12\x19\n\x11noise_floor_power\x18\x07 \x01(\r:\x04\x80\xa6\x1d\x01\"L\n\x1a\x45xternalAudioStreamPrepare\x12\x18\n\x10\x61udio_frame_rate\x18\x01 \x01(\r\x12\x14\n\x0c\x61udio_volume\x18\x02 \x01(\r\"W\n\x18\x45xternalAudioStreamChunk\x12\x1e\n\x16\x61udio_chunk_size_bytes\x18\x01 \x01(\r\x12\x1b\n\x13\x61udio_chunk_samples\x18\x02 \x01(\x0c\"\x1d\n\x1b\x45xternalAudioStreamComplete\"\x1b\n\x19\x45xternalAudioStreamCancel\"\x9e\x03\n\x1a\x45xternalAudioStreamRequest\x12Z\n\x14\x61udio_stream_prepare\x18\x01 \x01(\x0b\x32:.Anki.Vector.external_interface.ExternalAudioStreamPrepareH\x00\x12V\n\x12\x61udio_stream_chunk\x18\x02 \x01(\x0b\x32\x38.Anki.Vector.external_interface.ExternalAudioStreamChunkH\x00\x12\\\n\x15\x61udio_stream_complete\x18\x03 \x01(\x0b\x32;.Anki.Vector.external_interface.ExternalAudioStreamCompleteH\x00\x12X\n\x13\x61udio_stream_cancel\x18\x04 \x01(\x0b\x32\x39.Anki.Vector.external_interface.ExternalAudioStreamCancelH\x00\x42\x14\n\x12\x61udio_request_type\"%\n#ExternalAudioStreamPlaybackComplete\"$\n\"ExternalAudioStreamPlaybackFailure\"\\\n ExternalAudioStreamBufferOverrun\x12\x1a\n\x12\x61udio_samples_sent\x18\x01 \x01(\r\x12\x1c\n\x14\x61udio_samples_played\x18\x02 \x01(\r\"\xf9\x02\n\x1b\x45xternalAudioStreamResponse\x12m\n\x1e\x61udio_stream_playback_complete\x18\x01 \x01(\x0b\x32\x43.Anki.Vector.external_interface.ExternalAudioStreamPlaybackCompleteH\x00\x12g\n\x1b\x61udio_stream_buffer_overrun\x18\x02 \x01(\x0b\x32@.Anki.Vector.external_interface.ExternalAudioStreamBufferOverrunH\x00\x12k\n\x1d\x61udio_stream_playback_failyer\x18\x03 \x01(\x0b\x32\x42.Anki.Vector.external_interface.ExternalAudioStreamPlaybackFailureH\x00\x42\x15\n\x13\x61udio_response_type\"^\n\x13MasterVolumeRequest\x12G\n\x0cvolume_level\x18\x01 \x01(\x0e\x32\x31.Anki.Vector.external_interface.MasterVolumeLevel\"V\n\x14MasterVolumeResponse\x12>\n\x06status\x18\x01 \x01(\x0b\x32..Anki.Vector.external_interface.ResponseStatus\".\n\x1c\x45nableMarkerDetectionRequest\x12\x0e\n\x06\x65nable\x18\x01 \x01(\x08\"_\n\x1d\x45nableMarkerDetectionResponse\x12>\n\x06status\x18\x01 \x01(\x0b\x32..Anki.Vector.external_interface.ResponseStatus\"\xb1\x01\n\x1a\x45nableFaceDetectionRequest\x12\x0e\n\x06\x65nable\x18\x01 \x01(\x08\x12\x1e\n\x16\x65nable_smile_detection\x18\x02 \x01(\x08\x12$\n\x1c\x65nable_expression_estimation\x18\x03 \x01(\x08\x12\x1e\n\x16\x65nable_blink_detection\x18\x04 \x01(\x08\x12\x1d\n\x15\x65nable_gaze_detection\x18\x05 \x01(\x08\"]\n\x1b\x45nableFaceDetectionResponse\x12>\n\x06status\x18\x01 \x01(\x0b\x32..Anki.Vector.external_interface.ResponseStatus\".\n\x1c\x45nableMotionDetectionRequest\x12\x0e\n\x06\x65nable\x18\x01 \x01(\x08\"_\n\x1d\x45nableMotionDetectionResponse\x12>\n\x06status\x18\x01 \x01(\x0b\x32..Anki.Vector.external_interface.ResponseStatus\")\n\x17\x45nableMirrorModeRequest\x12\x0e\n\x06\x65nable\x18\x01 \x01(\x08\"Z\n\x18\x45nableMirrorModeResponse\x12>\n\x06status\x18\x01 \x01(\x0b\x32..Anki.Vector.external_interface.ResponseStatus\"\x14\n\x12MirrorModeDisabled\"M\n\x1b\x45nableImageStreamingRequest\x12\x0e\n\x06\x65nable\x18\x01 \x01(\x08\x12\x1e\n\x16\x65nable_high_resolution\x18\x02 \x01(\x08\"^\n\x1c\x45nableImageStreamingResponse\x12>\n\x06status\x18\x01 \x01(\x0b\x32..Anki.Vector.external_interface.ResponseStatus\" \n\x1eIsImageStreamingEnabledRequest\"E\n\x1fIsImageStreamingEnabledResponse\x12\"\n\x1ais_image_streaming_enabled\x18\x01 \x01(\x08\"\x19\n\x17VisionModesAutoDisabled\"\xd1\x03\n\nImageChunk\x12\x18\n\x10\x66rame_time_stamp\x18\x01 \x01(\r\x12\x10\n\x08image_id\x18\x02 \x01(\r\x12\r\n\x05width\x18\x03 \x01(\r\x12\x0e\n\x06height\x18\x04 \x01(\r\x12P\n\x0eimage_encoding\x18\x05 \x01(\x0e\x32\x38.Anki.Vector.external_interface.ImageChunk.ImageEncoding\x12\x15\n\rdisplay_index\x18\x06 \x01(\r\x12\x19\n\x11image_chunk_count\x18\x07 \x01(\r\x12\x10\n\x08\x63hunk_id\x18\x08 \x01(\r\x12\x0c\n\x04\x64\x61ta\x18\t \x01(\x0c\"\xd3\x01\n\rImageEncoding\x12\x17\n\x13NONE_IMAGE_ENCODING\x10\x00\x12\x0c\n\x08RAW_GRAY\x10\x01\x12\x0b\n\x07RAW_RGB\x10\x02\x12\x08\n\x04YUYV\x10\x03\x12\x0c\n\x08YUV420SP\x10\x04\x12\t\n\x05\x42\x41YER\x10\x05\x12\r\n\tJPEG_GRAY\x10\x06\x12\x0e\n\nJPEG_COLOR\x10\x07\x12\x19\n\x15JPEG_COLOR_HALF_WIDTH\x10\x08\x12\x17\n\x13JPEG_MINIMIZED_GRAY\x10\t\x12\x18\n\x14JPEG_MINIMIZED_COLOR\x10\n\"\x13\n\x11\x43\x61meraFeedRequest\"\xa6\x01\n\x12\x43\x61meraFeedResponse\x12\x18\n\x10\x66rame_time_stamp\x18\x01 \x01(\r\x12\x10\n\x08image_id\x18\x02 \x01(\r\x12P\n\x0eimage_encoding\x18\x03 \x01(\x0e\x32\x38.Anki.Vector.external_interface.ImageChunk.ImageEncoding\x12\x0c\n\x04\x64\x61ta\x18\x04 \x01(\x0c:\x04\x80\xa6\x1d\x01\";\n\x19\x43\x61ptureSingleImageRequest\x12\x1e\n\x16\x65nable_high_resolution\x18\x01 \x01(\x08\"\xe8\x01\n\x1a\x43\x61ptureSingleImageResponse\x12>\n\x06status\x18\x01 \x01(\x0b\x32..Anki.Vector.external_interface.ResponseStatus\x12\x18\n\x10\x66rame_time_stamp\x18\x02 \x01(\r\x12\x10\n\x08image_id\x18\x03 \x01(\r\x12P\n\x0eimage_encoding\x18\x04 \x01(\x0e\x32\x38.Anki.Vector.external_interface.ImageChunk.ImageEncoding\x12\x0c\n\x04\x64\x61ta\x18\x05 \x01(\x0c\"5\n\x12SetEyeColorRequest\x12\x0b\n\x03hue\x18\x01 \x01(\x02\x12\x12\n\nsaturation\x18\x02 \x01(\x02\"U\n\x13SetEyeColorResponse\x12>\n\x06status\x18\x01 \x01(\x0b\x32..Anki.Vector.external_interface.ResponseStatus\"\x15\n\x13\x43\x61meraConfigRequest\"\x84\x02\n\x14\x43\x61meraConfigResponse\x12\x16\n\x0e\x66ocal_length_x\x18\x01 \x01(\x02\x12\x16\n\x0e\x66ocal_length_y\x18\x02 \x01(\x02\x12\x10\n\x08\x63\x65nter_x\x18\x03 \x01(\x02\x12\x10\n\x08\x63\x65nter_y\x18\x04 \x01(\x02\x12\r\n\x05\x66ov_x\x18\x05 \x01(\x02\x12\r\n\x05\x66ov_y\x18\x06 \x01(\x02\x12#\n\x1bmin_camera_exposure_time_ms\x18\x07 \x01(\r\x12#\n\x1bmax_camera_exposure_time_ms\x18\x08 \x01(\r\x12\x17\n\x0fmin_camera_gain\x18\t \x01(\x02\x12\x17\n\x0fmax_camera_gain\x18\n \x01(\x02\"[\n\x18SetCameraSettingsRequest\x12\x0c\n\x04gain\x18\x01 \x01(\x02\x12\x13\n\x0b\x65xposure_ms\x18\x02 \x01(\r\x12\x1c\n\x14\x65nable_auto_exposure\x18\x03 \x01(\x08\"s\n\x19SetCameraSettingsResponse\x12>\n\x06status\x18\x01 \x01(\x0b\x32..Anki.Vector.external_interface.ResponseStatus\x12\x16\n\x0estatus_message\x18\x02 \x01(\t\"X\n\x14\x43\x61meraSettingsUpdate\x12\x0c\n\x04gain\x18\x01 \x01(\x02\x12\x13\n\x0b\x65xposure_ms\x18\x02 \x01(\r\x12\x1d\n\x15\x61uto_exposure_enabled\x18\x03 \x01(\x08\"\x96\x01\n\x18SDKInitializationRequest\x12\x1a\n\x12sdk_module_version\x18\x01 \x01(\t\x12\x16\n\x0epython_version\x18\x02 \x01(\t\x12\x1d\n\x15python_implementation\x18\x03 \x01(\t\x12\x12\n\nos_version\x18\x04 \x01(\t\x12\x13\n\x0b\x63pu_version\x18\x05 \x01(\t\"[\n\x19SDKInitializationResponse\x12>\n\x06status\x18\x01 \x01(\x0b\x32..Anki.Vector.external_interface.ResponseStatus*\xdc\x04\n\x0bRobotStatus\x12\x15\n\x11ROBOT_STATUS_NONE\x10\x00\x12\x1a\n\x16ROBOT_STATUS_IS_MOVING\x10\x01\x12\"\n\x1eROBOT_STATUS_IS_CARRYING_BLOCK\x10\x02\x12&\n\"ROBOT_STATUS_IS_PICKING_OR_PLACING\x10\x04\x12\x1d\n\x19ROBOT_STATUS_IS_PICKED_UP\x10\x08\x12\"\n\x1eROBOT_STATUS_IS_BUTTON_PRESSED\x10\x10\x12\x1b\n\x17ROBOT_STATUS_IS_FALLING\x10 \x12\x1d\n\x19ROBOT_STATUS_IS_ANIMATING\x10@\x12\x1c\n\x17ROBOT_STATUS_IS_PATHING\x10\x80\x01\x12\x1d\n\x18ROBOT_STATUS_LIFT_IN_POS\x10\x80\x02\x12\x1d\n\x18ROBOT_STATUS_HEAD_IN_POS\x10\x80\x04\x12!\n\x1cROBOT_STATUS_CALM_POWER_MODE\x10\x80\x08\x12\x1f\n\x1aROBOT_STATUS_IS_ON_CHARGER\x10\x80 \x12\x1d\n\x18ROBOT_STATUS_IS_CHARGING\x10\x80@\x12!\n\x1bROBOT_STATUS_CLIFF_DETECTED\x10\x80\x80\x01\x12$\n\x1eROBOT_STATUS_ARE_WHEELS_MOVING\x10\x80\x80\x02\x12 \n\x1aROBOT_STATUS_IS_BEING_HELD\x10\x80\x80\x04\x12%\n\x1fROBOT_STATUS_IS_MOTION_DETECTED\x10\x80\x80\x08*\x8d\x01\n\x16UnexpectedMovementType\x12\x16\n\x12TURNED_BUT_STOPPED\x10\x00\x12\x1c\n\x18TURNED_IN_SAME_DIRECTION\x10\x01\x12 \n\x1cTURNED_IN_OPPOSITE_DIRECTION\x10\x02\x12\x1b\n\x17ROTATING_WITHOUT_MOTORS\x10\x03*O\n\x16UnexpectedMovementSide\x12\x0b\n\x07UNKNOWN\x10\x00\x12\t\n\x05\x46RONT\x10\x01\x12\x08\n\x04\x42\x41\x43K\x10\x02\x12\x08\n\x04LEFT\x10\x03\x12\t\n\x05RIGHT\x10\x04*\xbd\x01\n\x10\x46\x61\x63ialExpression\x12\x16\n\x12\x45XPRESSION_UNKNOWN\x10\x00\x12\x16\n\x12\x45XPRESSION_NEUTRAL\x10\x01\x12\x18\n\x14\x45XPRESSION_HAPPINESS\x10\x02\x12\x17\n\x13\x45XPRESSION_SURPRISE\x10\x03\x12\x14\n\x10\x45XPRESSION_ANGER\x10\x04\x12\x16\n\x12\x45XPRESSION_SADNESS\x10\x05\x12\x14\n\x10\x45XPRESSION_COUNT\x10\x05\x1a\x02\x10\x01*\xcc\x01\n\x14\x46\x61\x63\x65\x45nrollmentResult\x12\x0b\n\x07SUCCESS\x10\x00\x12\x12\n\x0eSAW_WRONG_FACE\x10\x01\x12\x16\n\x12SAW_MULTIPLE_FACES\x10\x02\x12\r\n\tTIMED_OUT\x10\x03\x12\x0f\n\x0bSAVE_FAILED\x10\x04\x12\x0e\n\nINCOMPLETE\x10\x05\x12\r\n\tCANCELLED\x10\x06\x12\x0f\n\x0bNAME_IN_USE\x10\x07\x12\x16\n\x12NAMED_STORAGE_FULL\x10\x08\x12\x13\n\x0fUNKNOWN_FAILURE\x10\t*l\n\x0f\x42\x65haviorResults\x12\x1a\n\x16\x42\x45HAVIOR_INVALID_STATE\x10\x00\x12\x1b\n\x17\x42\x45HAVIOR_COMPLETE_STATE\x10\x01\x12 \n\x1c\x42\x45HAVIOR_WONT_ACTIVATE_STATE\x10\x02*S\n\x12\x41\x63tionTagConstants\x12\x13\n\x0fINVALID_SDK_TAG\x10\x00\x12\x13\n\rFIRST_SDK_TAG\x10\x81\x89z\x12\x13\n\x0cLAST_SDK_TAG\x10\xc0\x8d\xb7\x01*\x9e\x01\n\rAlignmentType\x12\x1a\n\x16\x41LIGNMENT_TYPE_UNKNOWN\x10\x00\x12\x1e\n\x1a\x41LIGNMENT_TYPE_LIFT_FINGER\x10\x01\x12\x1d\n\x19\x41LIGNMENT_TYPE_LIFT_PLATE\x10\x02\x12\x17\n\x13\x41LIGNMENT_TYPE_BODY\x10\x03\x12\x19\n\x15\x41LIGNMENT_TYPE_CUSTOM\x10\x04*s\n\x0c\x42\x61tteryLevel\x12\x19\n\x15\x42\x41TTERY_LEVEL_UNKNOWN\x10\x00\x12\x15\n\x11\x42\x41TTERY_LEVEL_LOW\x10\x01\x12\x19\n\x15\x42\x41TTERY_LEVEL_NOMINAL\x10\x02\x12\x16\n\x12\x42\x41TTERY_LEVEL_FULL\x10\x03*\xcc\x01\n\x0e\x41udioConstants\x12\x18\n\x14\x41UDIO_CONSTANTS_NULL\x10\x00\x12\x1c\n\x18MIC_DETECTION_DIRECTIONS\x10\x0c\x12%\n SAMPLE_COUNTS_PER_ENGINE_MESSAGE\x10\xa0\x01\x12\"\n\x1dSAMPLE_COUNTS_PER_SDK_MESSAGE\x10\xc0\x0c\x12\x1b\n\x16MICROPHONE_SAMPLE_RATE\x10\x89z\x12\x1a\n\x15PROCESSED_SAMPLE_RATE\x10\x80}*\x85\x01\n\x13\x41udioProcessingMode\x12\x11\n\rAUDIO_UNKNOWN\x10\x00\x12\r\n\tAUDIO_OFF\x10\x01\x12\x13\n\x0f\x41UDIO_FAST_MODE\x10\x02\x12\x1a\n\x16\x41UDIO_DIRECTIONAL_MODE\x10\x03\x12\x1b\n\x17\x41UDIO_VOICE_DETECT_MODE\x10\x04*v\n\x11MasterVolumeLevel\x12\x0e\n\nVOLUME_LOW\x10\x00\x12\x15\n\x11VOLUME_MEDIUM_LOW\x10\x01\x12\x11\n\rVOLUME_MEDIUM\x10\x02\x12\x16\n\x12VOLUME_MEDIUM_HIGH\x10\x03\x12\x0f\n\x0bVOLUME_HIGH\x10\x04\x62\x06proto3') , dependencies=[anki__vector_dot_messaging_dot_response__status__pb2.DESCRIPTOR,anki__vector_dot_messaging_dot_extensions__pb2.DESCRIPTOR,]) @@ -107,12 +107,78 @@ ], containing_type=None, options=None, - serialized_start=19443, - serialized_end=20047, + serialized_start=21067, + serialized_end=21671, ) _sym_db.RegisterEnumDescriptor(_ROBOTSTATUS) RobotStatus = enum_type_wrapper.EnumTypeWrapper(_ROBOTSTATUS) +_UNEXPECTEDMOVEMENTTYPE = _descriptor.EnumDescriptor( + name='UnexpectedMovementType', + full_name='Anki.Vector.external_interface.UnexpectedMovementType', + filename=None, + file=DESCRIPTOR, + values=[ + _descriptor.EnumValueDescriptor( + name='TURNED_BUT_STOPPED', index=0, number=0, + options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='TURNED_IN_SAME_DIRECTION', index=1, number=1, + options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='TURNED_IN_OPPOSITE_DIRECTION', index=2, number=2, + options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='ROTATING_WITHOUT_MOTORS', index=3, number=3, + options=None, + type=None), + ], + containing_type=None, + options=None, + serialized_start=21674, + serialized_end=21815, +) +_sym_db.RegisterEnumDescriptor(_UNEXPECTEDMOVEMENTTYPE) + +UnexpectedMovementType = enum_type_wrapper.EnumTypeWrapper(_UNEXPECTEDMOVEMENTTYPE) +_UNEXPECTEDMOVEMENTSIDE = _descriptor.EnumDescriptor( + name='UnexpectedMovementSide', + full_name='Anki.Vector.external_interface.UnexpectedMovementSide', + filename=None, + file=DESCRIPTOR, + values=[ + _descriptor.EnumValueDescriptor( + name='UNKNOWN', index=0, number=0, + options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='FRONT', index=1, number=1, + options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='BACK', index=2, number=2, + options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='LEFT', index=3, number=3, + options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='RIGHT', index=4, number=4, + options=None, + type=None), + ], + containing_type=None, + options=None, + serialized_start=21817, + serialized_end=21896, +) +_sym_db.RegisterEnumDescriptor(_UNEXPECTEDMOVEMENTSIDE) + +UnexpectedMovementSide = enum_type_wrapper.EnumTypeWrapper(_UNEXPECTEDMOVEMENTSIDE) _FACIALEXPRESSION = _descriptor.EnumDescriptor( name='FacialExpression', full_name='Anki.Vector.external_interface.FacialExpression', @@ -150,8 +216,8 @@ ], containing_type=None, options=_descriptor._ParseOptions(descriptor_pb2.EnumOptions(), _b('\020\001')), - serialized_start=20050, - serialized_end=20239, + serialized_start=21899, + serialized_end=22088, ) _sym_db.RegisterEnumDescriptor(_FACIALEXPRESSION) @@ -205,8 +271,8 @@ ], containing_type=None, options=None, - serialized_start=20242, - serialized_end=20446, + serialized_start=22091, + serialized_end=22295, ) _sym_db.RegisterEnumDescriptor(_FACEENROLLMENTRESULT) @@ -232,8 +298,8 @@ ], containing_type=None, options=None, - serialized_start=20448, - serialized_end=20556, + serialized_start=22297, + serialized_end=22405, ) _sym_db.RegisterEnumDescriptor(_BEHAVIORRESULTS) @@ -259,8 +325,8 @@ ], containing_type=None, options=None, - serialized_start=20558, - serialized_end=20641, + serialized_start=22407, + serialized_end=22490, ) _sym_db.RegisterEnumDescriptor(_ACTIONTAGCONSTANTS) @@ -294,8 +360,8 @@ ], containing_type=None, options=None, - serialized_start=20644, - serialized_end=20802, + serialized_start=22493, + serialized_end=22651, ) _sym_db.RegisterEnumDescriptor(_ALIGNMENTTYPE) @@ -325,8 +391,8 @@ ], containing_type=None, options=None, - serialized_start=20804, - serialized_end=20919, + serialized_start=22653, + serialized_end=22768, ) _sym_db.RegisterEnumDescriptor(_BATTERYLEVEL) @@ -364,8 +430,8 @@ ], containing_type=None, options=None, - serialized_start=20922, - serialized_end=21126, + serialized_start=22771, + serialized_end=22975, ) _sym_db.RegisterEnumDescriptor(_AUDIOCONSTANTS) @@ -399,8 +465,8 @@ ], containing_type=None, options=None, - serialized_start=21129, - serialized_end=21262, + serialized_start=22978, + serialized_end=23111, ) _sym_db.RegisterEnumDescriptor(_AUDIOPROCESSINGMODE) @@ -434,8 +500,8 @@ ], containing_type=None, options=None, - serialized_start=21264, - serialized_end=21382, + serialized_start=23113, + serialized_end=23231, ) _sym_db.RegisterEnumDescriptor(_MASTERVOLUMELEVEL) @@ -458,6 +524,15 @@ ROBOT_STATUS_ARE_WHEELS_MOVING = 32768 ROBOT_STATUS_IS_BEING_HELD = 65536 ROBOT_STATUS_IS_MOTION_DETECTED = 131072 +TURNED_BUT_STOPPED = 0 +TURNED_IN_SAME_DIRECTION = 1 +TURNED_IN_OPPOSITE_DIRECTION = 2 +ROTATING_WITHOUT_MOTORS = 3 +UNKNOWN = 0 +FRONT = 1 +BACK = 2 +LEFT = 3 +RIGHT = 4 EXPRESSION_UNKNOWN = 0 EXPRESSION_NEUTRAL = 1 EXPRESSION_HAPPINESS = 2 @@ -729,8 +804,8 @@ ], containing_type=None, options=None, - serialized_start=8147, - serialized_end=9709, + serialized_start=8984, + serialized_end=10546, ) _sym_db.RegisterEnumDescriptor(_ACTIONRESULT_ACTIONRESULTCODE) @@ -751,8 +826,8 @@ ], containing_type=None, options=None, - serialized_start=14429, - serialized_end=14468, + serialized_start=15381, + serialized_end=15420, ) _sym_db.RegisterEnumDescriptor(_CUBEBATTERY_CUBEBATTERYLEVEL) @@ -785,8 +860,8 @@ ], containing_type=None, options=None, - serialized_start=14873, - serialized_end=14956, + serialized_start=15847, + serialized_end=15930, ) _sym_db.RegisterEnumDescriptor(_SAYTEXTRESPONSE_UTTERANCESTATE) @@ -843,8 +918,8 @@ ], containing_type=None, options=None, - serialized_start=18387, - serialized_end=18598, + serialized_start=19393, + serialized_end=19604, ) _sym_db.RegisterEnumDescriptor(_IMAGECHUNK_IMAGEENCODING) @@ -2242,6 +2317,246 @@ ) +_ROBOTOBSERVEDMOTION = _descriptor.Descriptor( + name='RobotObservedMotion', + full_name='Anki.Vector.external_interface.RobotObservedMotion', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='timestamp', full_name='Anki.Vector.external_interface.RobotObservedMotion.timestamp', index=0, + number=1, type=13, cpp_type=3, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='img_area', full_name='Anki.Vector.external_interface.RobotObservedMotion.img_area', index=1, + number=2, type=2, cpp_type=6, label=1, + has_default_value=False, default_value=float(0), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='img_x', full_name='Anki.Vector.external_interface.RobotObservedMotion.img_x', index=2, + number=3, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='img_y', full_name='Anki.Vector.external_interface.RobotObservedMotion.img_y', index=3, + number=4, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='ground_area', full_name='Anki.Vector.external_interface.RobotObservedMotion.ground_area', index=4, + number=5, type=2, cpp_type=6, label=1, + has_default_value=False, default_value=float(0), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='ground_x', full_name='Anki.Vector.external_interface.RobotObservedMotion.ground_x', index=5, + number=6, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='ground_y', full_name='Anki.Vector.external_interface.RobotObservedMotion.ground_y', index=6, + number=7, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='top_img_area', full_name='Anki.Vector.external_interface.RobotObservedMotion.top_img_area', index=7, + number=8, type=2, cpp_type=6, label=1, + has_default_value=False, default_value=float(0), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='top_img_x', full_name='Anki.Vector.external_interface.RobotObservedMotion.top_img_x', index=8, + number=9, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='top_img_y', full_name='Anki.Vector.external_interface.RobotObservedMotion.top_img_y', index=9, + number=10, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='bottom_img_area', full_name='Anki.Vector.external_interface.RobotObservedMotion.bottom_img_area', index=10, + number=11, type=2, cpp_type=6, label=1, + has_default_value=False, default_value=float(0), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='bottom_img_x', full_name='Anki.Vector.external_interface.RobotObservedMotion.bottom_img_x', index=11, + number=12, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='bottom_img_y', full_name='Anki.Vector.external_interface.RobotObservedMotion.bottom_img_y', index=12, + number=13, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='left_img_area', full_name='Anki.Vector.external_interface.RobotObservedMotion.left_img_area', index=13, + number=14, type=2, cpp_type=6, label=1, + has_default_value=False, default_value=float(0), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='left_img_x', full_name='Anki.Vector.external_interface.RobotObservedMotion.left_img_x', index=14, + number=15, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='left_img_y', full_name='Anki.Vector.external_interface.RobotObservedMotion.left_img_y', index=15, + number=16, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='right_img_area', full_name='Anki.Vector.external_interface.RobotObservedMotion.right_img_area', index=16, + number=17, type=2, cpp_type=6, label=1, + has_default_value=False, default_value=float(0), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='right_img_x', full_name='Anki.Vector.external_interface.RobotObservedMotion.right_img_x', index=17, + number=18, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='right_img_y', full_name='Anki.Vector.external_interface.RobotObservedMotion.right_img_y', index=18, + number=19, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=3852, + serialized_end=4255, +) + + +_ROBOTERASEDENROLLEDFACE = _descriptor.Descriptor( + name='RobotErasedEnrolledFace', + full_name='Anki.Vector.external_interface.RobotErasedEnrolledFace', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='face_id', full_name='Anki.Vector.external_interface.RobotErasedEnrolledFace.face_id', index=0, + number=1, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='name', full_name='Anki.Vector.external_interface.RobotErasedEnrolledFace.name', index=1, + number=2, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=4257, + serialized_end=4313, +) + + +_UNEXPECTEDMOVEMENT = _descriptor.Descriptor( + name='UnexpectedMovement', + full_name='Anki.Vector.external_interface.UnexpectedMovement', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='timestamp', full_name='Anki.Vector.external_interface.UnexpectedMovement.timestamp', index=0, + number=1, type=13, cpp_type=3, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='movement_type', full_name='Anki.Vector.external_interface.UnexpectedMovement.movement_type', index=1, + number=2, type=14, cpp_type=8, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='movement_side', full_name='Anki.Vector.external_interface.UnexpectedMovement.movement_side', index=2, + number=3, type=14, cpp_type=8, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=4316, + serialized_end=4513, +) + + _ROBOTOBSERVEDFACE = _descriptor.Descriptor( name='RobotObservedFace', full_name='Anki.Vector.external_interface.RobotObservedFace', @@ -2338,8 +2653,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=3852, - serialized_end=4374, + serialized_start=4516, + serialized_end=5038, ) @@ -2376,8 +2691,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=4376, - serialized_end=4436, + serialized_start=5040, + serialized_end=5100, ) @@ -2421,8 +2736,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=4438, - serialized_end=4564, + serialized_start=5102, + serialized_end=5228, ) @@ -2445,8 +2760,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=4566, - serialized_end=4595, + serialized_start=5230, + serialized_end=5259, ) @@ -2476,8 +2791,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=4597, - serialized_end=4691, + serialized_start=5261, + serialized_end=5355, ) @@ -2500,8 +2815,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=4693, - serialized_end=4722, + serialized_start=5357, + serialized_end=5386, ) @@ -2566,8 +2881,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=4725, - serialized_end=4919, + serialized_start=5389, + serialized_end=5583, ) @@ -2604,8 +2919,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=4921, - serialized_end=4978, + serialized_start=5585, + serialized_end=5642, ) @@ -2642,8 +2957,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=4981, - serialized_end=5139, + serialized_start=5645, + serialized_end=5803, ) @@ -2687,8 +3002,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=5141, - serialized_end=5225, + serialized_start=5805, + serialized_end=5889, ) @@ -2718,8 +3033,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=5227, - serialized_end=5323, + serialized_start=5891, + serialized_end=5987, ) @@ -2749,8 +3064,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=5325, - serialized_end=5372, + serialized_start=5989, + serialized_end=6036, ) @@ -2780,8 +3095,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=5374, - serialized_end=5469, + serialized_start=6038, + serialized_end=6133, ) @@ -2804,8 +3119,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=5471, - serialized_end=5501, + serialized_start=6135, + serialized_end=6165, ) @@ -2835,8 +3150,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=5503, - serialized_end=5598, + serialized_start=6167, + serialized_end=6262, ) @@ -2901,8 +3216,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=5601, - serialized_end=5737, + serialized_start=6265, + serialized_end=6401, ) @@ -2932,8 +3247,70 @@ extension_ranges=[], oneofs=[ ], - serialized_start=5739, - serialized_end=5828, + serialized_start=6403, + serialized_end=6492, +) + + +_ENROLLFACEREQUEST = _descriptor.Descriptor( + name='EnrollFaceRequest', + full_name='Anki.Vector.external_interface.EnrollFaceRequest', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=6494, + serialized_end=6513, +) + + +_ENROLLFACERESPONSE = _descriptor.Descriptor( + name='EnrollFaceResponse', + full_name='Anki.Vector.external_interface.EnrollFaceResponse', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='status', full_name='Anki.Vector.external_interface.EnrollFaceResponse.status', index=0, + number=1, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='result', full_name='Anki.Vector.external_interface.EnrollFaceResponse.result', index=1, + number=2, type=14, cpp_type=8, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=6516, + serialized_end=6665, ) @@ -2956,8 +3333,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=5830, - serialized_end=5854, + serialized_start=6667, + serialized_end=6691, ) @@ -2994,8 +3371,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=5857, - serialized_end=6011, + serialized_start=6694, + serialized_end=6848, ) @@ -3018,8 +3395,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=6013, - serialized_end=6036, + serialized_start=6850, + serialized_end=6873, ) @@ -3056,8 +3433,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=6039, - serialized_end=6192, + serialized_start=6876, + serialized_end=7029, ) @@ -3080,8 +3457,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=6194, - serialized_end=6212, + serialized_start=7031, + serialized_end=7049, ) @@ -3118,8 +3495,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=6215, - serialized_end=6363, + serialized_start=7052, + serialized_end=7200, ) @@ -3142,8 +3519,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=6365, - serialized_end=6391, + serialized_start=7202, + serialized_end=7228, ) @@ -3180,8 +3557,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=6394, - serialized_end=6550, + serialized_start=7231, + serialized_end=7387, ) @@ -3204,8 +3581,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=6552, - serialized_end=6570, + serialized_start=7389, + serialized_end=7407, ) @@ -3242,8 +3619,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=6573, - serialized_end=6721, + serialized_start=7410, + serialized_end=7558, ) @@ -3266,8 +3643,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=6723, - serialized_end=6738, + serialized_start=7560, + serialized_end=7575, ) @@ -3304,8 +3681,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=6740, - serialized_end=6796, + serialized_start=7577, + serialized_end=7633, ) @@ -3342,8 +3719,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=6798, - serialized_end=6848, + serialized_start=7635, + serialized_end=7685, ) @@ -3394,8 +3771,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=6850, - serialized_end=6960, + serialized_start=7687, + serialized_end=7797, ) @@ -3418,8 +3795,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=6962, - serialized_end=6981, + serialized_start=7799, + serialized_end=7818, ) @@ -3456,8 +3833,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=6984, - serialized_end=7132, + serialized_start=7821, + serialized_end=7969, ) @@ -3487,8 +3864,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=7134, - serialized_end=7166, + serialized_start=7971, + serialized_end=8003, ) @@ -3525,8 +3902,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=7168, - serialized_end=7222, + serialized_start=8005, + serialized_end=8059, ) @@ -3570,8 +3947,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=7224, - serialized_end=7335, + serialized_start=8061, + serialized_end=8172, ) @@ -3601,8 +3978,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=7337, - serialized_end=7373, + serialized_start=8174, + serialized_end=8210, ) @@ -3639,8 +4016,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=7375, - serialized_end=7433, + serialized_start=8212, + serialized_end=8270, ) @@ -3684,8 +4061,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=7435, - serialized_end=7550, + serialized_start=8272, + serialized_end=8387, ) @@ -3715,8 +4092,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=7552, - serialized_end=7590, + serialized_start=8389, + serialized_end=8427, ) @@ -3753,8 +4130,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=7592, - serialized_end=7694, + serialized_start=8429, + serialized_end=8531, ) @@ -3784,8 +4161,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=7696, - serialized_end=7726, + serialized_start=8533, + serialized_end=8563, ) @@ -3885,8 +4262,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=7729, - serialized_end=8050, + serialized_start=8566, + serialized_end=8887, ) @@ -3917,8 +4294,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=8053, - serialized_end=9709, + serialized_start=8890, + serialized_end=10546, ) @@ -3948,8 +4325,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=9711, - serialized_end=9755, + serialized_start=10548, + serialized_end=10592, ) @@ -3979,8 +4356,63 @@ extension_ranges=[], oneofs=[ ], - serialized_start=9757, - serialized_end=9850, + serialized_start=10594, + serialized_end=10687, +) + + +_CANCELBEHAVIORREQUEST = _descriptor.Descriptor( + name='CancelBehaviorRequest', + full_name='Anki.Vector.external_interface.CancelBehaviorRequest', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=10689, + serialized_end=10712, +) + + +_CANCELBEHAVIORRESPONSE = _descriptor.Descriptor( + name='CancelBehaviorResponse', + full_name='Anki.Vector.external_interface.CancelBehaviorResponse', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='status', full_name='Anki.Vector.external_interface.CancelBehaviorResponse.status', index=0, + number=1, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=10714, + serialized_end=10802, ) @@ -4045,8 +4477,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=9853, - serialized_end=10020, + serialized_start=10805, + serialized_end=10972, ) @@ -4083,8 +4515,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=10023, - serialized_end=10167, + serialized_start=10975, + serialized_end=11119, ) @@ -4170,8 +4602,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=10170, - serialized_end=10506, + serialized_start=11122, + serialized_end=11458, ) @@ -4208,8 +4640,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=10509, - serialized_end=10657, + serialized_start=11461, + serialized_end=11609, ) @@ -4267,8 +4699,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=10659, - serialized_end=10786, + serialized_start=11611, + serialized_end=11738, ) @@ -4305,8 +4737,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=10789, - serialized_end=10938, + serialized_start=11741, + serialized_end=11890, ) @@ -4378,8 +4810,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=10941, - serialized_end=11110, + serialized_start=11893, + serialized_end=12062, ) @@ -4416,8 +4848,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=11113, - serialized_end=11260, + serialized_start=12065, + serialized_end=12212, ) @@ -4482,8 +4914,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=11263, - serialized_end=11421, + serialized_start=12215, + serialized_end=12373, ) @@ -4520,8 +4952,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=11424, - serialized_end=11572, + serialized_start=12376, + serialized_end=12524, ) @@ -4586,8 +5018,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=11575, - serialized_end=11734, + serialized_start=12527, + serialized_end=12686, ) @@ -4624,8 +5056,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=11737, - serialized_end=11886, + serialized_start=12689, + serialized_end=12838, ) @@ -4676,8 +5108,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=11888, - serialized_end=11994, + serialized_start=12840, + serialized_end=12946, ) @@ -4714,8 +5146,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=11997, - serialized_end=12148, + serialized_start=12949, + serialized_end=13100, ) @@ -4780,8 +5212,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=12151, - serialized_end=12365, + serialized_start=13103, + serialized_end=13317, ) @@ -4818,8 +5250,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=12368, - serialized_end=12514, + serialized_start=13320, + serialized_end=13466, ) @@ -4891,8 +5323,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=12517, - serialized_end=12747, + serialized_start=13469, + serialized_end=13699, ) @@ -4929,8 +5361,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=12750, - serialized_end=12896, + serialized_start=13702, + serialized_end=13848, ) @@ -5002,8 +5434,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=12899, - serialized_end=13130, + serialized_start=13851, + serialized_end=14082, ) @@ -5040,8 +5472,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=13133, - serialized_end=13280, + serialized_start=14085, + serialized_end=14232, ) @@ -5113,8 +5545,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=13283, - serialized_end=13515, + serialized_start=14235, + serialized_end=14467, ) @@ -5151,8 +5583,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=13518, - serialized_end=13666, + serialized_start=14470, + serialized_end=14618, ) @@ -5189,8 +5621,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=13668, - serialized_end=13737, + serialized_start=14620, + serialized_end=14689, ) @@ -5227,8 +5659,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=13740, - serialized_end=13899, + serialized_start=14692, + serialized_end=14851, ) @@ -5251,8 +5683,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=13901, - serialized_end=13922, + serialized_start=14853, + serialized_end=14874, ) @@ -5324,8 +5756,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=13925, - serialized_end=14254, + serialized_start=14877, + serialized_end=15206, ) @@ -5377,8 +5809,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=14257, - serialized_end=14468, + serialized_start=15209, + serialized_end=15420, ) @@ -5401,8 +5833,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=14470, - serialized_end=14491, + serialized_start=15422, + serialized_end=15443, ) @@ -5446,8 +5878,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=14494, - serialized_end=14625, + serialized_start=15446, + serialized_end=15577, ) @@ -5473,8 +5905,15 @@ is_extension=False, extension_scope=None, options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( - name='duration_scalar', full_name='Anki.Vector.external_interface.SayTextRequest.duration_scalar', index=2, - number=3, type=2, cpp_type=6, label=1, + name='duration_scalar', full_name='Anki.Vector.external_interface.SayTextRequest.duration_scalar', index=2, + number=3, type=2, cpp_type=6, label=1, + has_default_value=False, default_value=float(0), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='pitch_scalar', full_name='Anki.Vector.external_interface.SayTextRequest.pitch_scalar', index=3, + number=4, type=2, cpp_type=6, label=1, has_default_value=False, default_value=float(0), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, @@ -5491,8 +5930,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=14627, - serialized_end=14708, + serialized_start=15579, + serialized_end=15682, ) @@ -5530,8 +5969,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=14711, - serialized_end=14956, + serialized_start=15685, + serialized_end=15930, ) @@ -5603,8 +6042,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=14959, - serialized_end=15114, + serialized_start=15933, + serialized_end=16088, ) @@ -5634,8 +6073,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=15116, - serialized_end=15205, + serialized_start=16090, + serialized_end=16179, ) @@ -5665,8 +6104,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=15207, - serialized_end=15296, + serialized_start=16181, + serialized_end=16270, ) @@ -5752,8 +6191,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=15299, - serialized_end=15531, + serialized_start=16273, + serialized_end=16505, ) @@ -5776,8 +6215,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=15533, - serialized_end=15551, + serialized_start=16507, + serialized_end=16525, ) @@ -5849,8 +6288,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=15554, - serialized_end=15754, + serialized_start=16528, + serialized_end=16728, ) @@ -5887,8 +6326,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=15756, - serialized_end=15832, + serialized_start=16730, + serialized_end=16806, ) @@ -5925,8 +6364,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=15834, - serialized_end=15921, + serialized_start=16808, + serialized_end=16895, ) @@ -5949,8 +6388,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=15923, - serialized_end=15952, + serialized_start=16897, + serialized_end=16926, ) @@ -5973,8 +6412,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=15954, - serialized_end=15981, + serialized_start=16928, + serialized_end=16955, ) @@ -6028,8 +6467,8 @@ name='audio_request_type', full_name='Anki.Vector.external_interface.ExternalAudioStreamRequest.audio_request_type', index=0, containing_type=None, fields=[]), ], - serialized_start=15984, - serialized_end=16398, + serialized_start=16958, + serialized_end=17372, ) @@ -6052,8 +6491,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=16400, - serialized_end=16437, + serialized_start=17374, + serialized_end=17411, ) @@ -6076,8 +6515,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=16439, - serialized_end=16475, + serialized_start=17413, + serialized_end=17449, ) @@ -6114,8 +6553,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=16477, - serialized_end=16569, + serialized_start=17451, + serialized_end=17543, ) @@ -6162,8 +6601,8 @@ name='audio_response_type', full_name='Anki.Vector.external_interface.ExternalAudioStreamResponse.audio_response_type', index=0, containing_type=None, fields=[]), ], - serialized_start=16572, - serialized_end=16949, + serialized_start=17546, + serialized_end=17923, ) @@ -6193,8 +6632,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=16951, - serialized_end=17045, + serialized_start=17925, + serialized_end=18019, ) @@ -6224,8 +6663,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=17047, - serialized_end=17133, + serialized_start=18021, + serialized_end=18107, ) @@ -6255,8 +6694,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=17135, - serialized_end=17181, + serialized_start=18109, + serialized_end=18155, ) @@ -6286,8 +6725,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=17183, - serialized_end=17278, + serialized_start=18157, + serialized_end=18252, ) @@ -6345,8 +6784,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=17281, - serialized_end=17458, + serialized_start=18255, + serialized_end=18432, ) @@ -6376,8 +6815,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=17460, - serialized_end=17553, + serialized_start=18434, + serialized_end=18527, ) @@ -6407,8 +6846,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=17555, - serialized_end=17601, + serialized_start=18529, + serialized_end=18575, ) @@ -6438,8 +6877,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=17603, - serialized_end=17698, + serialized_start=18577, + serialized_end=18672, ) @@ -6469,8 +6908,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=17700, - serialized_end=17741, + serialized_start=18674, + serialized_end=18715, ) @@ -6500,8 +6939,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=17743, - serialized_end=17833, + serialized_start=18717, + serialized_end=18807, ) @@ -6524,8 +6963,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=17835, - serialized_end=17855, + serialized_start=18809, + serialized_end=18829, ) @@ -6543,6 +6982,13 @@ message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='enable_high_resolution', full_name='Anki.Vector.external_interface.EnableImageStreamingRequest.enable_high_resolution', index=1, + number=2, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), ], extensions=[ ], @@ -6555,8 +7001,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=17857, - serialized_end=17902, + serialized_start=18831, + serialized_end=18908, ) @@ -6586,8 +7032,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=17904, - serialized_end=17998, + serialized_start=18910, + serialized_end=19004, ) @@ -6610,8 +7056,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=18000, - serialized_end=18032, + serialized_start=19006, + serialized_end=19038, ) @@ -6641,8 +7087,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=18034, - serialized_end=18103, + serialized_start=19040, + serialized_end=19109, ) @@ -6665,8 +7111,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=18105, - serialized_end=18130, + serialized_start=19111, + serialized_end=19136, ) @@ -6753,8 +7199,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=18133, - serialized_end=18598, + serialized_start=19139, + serialized_end=19604, ) @@ -6777,8 +7223,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=18600, - serialized_end=18619, + serialized_start=19606, + serialized_end=19625, ) @@ -6829,8 +7275,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=18622, - serialized_end=18788, + serialized_start=19628, + serialized_end=19794, ) @@ -6841,6 +7287,13 @@ file=DESCRIPTOR, containing_type=None, fields=[ + _descriptor.FieldDescriptor( + name='enable_high_resolution', full_name='Anki.Vector.external_interface.CaptureSingleImageRequest.enable_high_resolution', index=0, + number=1, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), ], extensions=[ ], @@ -6853,8 +7306,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=18790, - serialized_end=18817, + serialized_start=19796, + serialized_end=19855, ) @@ -6912,8 +7365,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=18820, - serialized_end=19052, + serialized_start=19858, + serialized_end=20090, ) @@ -6950,8 +7403,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=19054, - serialized_end=19107, + serialized_start=20092, + serialized_end=20145, ) @@ -6981,8 +7434,254 @@ extension_ranges=[], oneofs=[ ], - serialized_start=19109, - serialized_end=19194, + serialized_start=20147, + serialized_end=20232, +) + + +_CAMERACONFIGREQUEST = _descriptor.Descriptor( + name='CameraConfigRequest', + full_name='Anki.Vector.external_interface.CameraConfigRequest', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=20234, + serialized_end=20255, +) + + +_CAMERACONFIGRESPONSE = _descriptor.Descriptor( + name='CameraConfigResponse', + full_name='Anki.Vector.external_interface.CameraConfigResponse', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='focal_length_x', full_name='Anki.Vector.external_interface.CameraConfigResponse.focal_length_x', index=0, + number=1, type=2, cpp_type=6, label=1, + has_default_value=False, default_value=float(0), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='focal_length_y', full_name='Anki.Vector.external_interface.CameraConfigResponse.focal_length_y', index=1, + number=2, type=2, cpp_type=6, label=1, + has_default_value=False, default_value=float(0), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='center_x', full_name='Anki.Vector.external_interface.CameraConfigResponse.center_x', index=2, + number=3, type=2, cpp_type=6, label=1, + has_default_value=False, default_value=float(0), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='center_y', full_name='Anki.Vector.external_interface.CameraConfigResponse.center_y', index=3, + number=4, type=2, cpp_type=6, label=1, + has_default_value=False, default_value=float(0), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='fov_x', full_name='Anki.Vector.external_interface.CameraConfigResponse.fov_x', index=4, + number=5, type=2, cpp_type=6, label=1, + has_default_value=False, default_value=float(0), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='fov_y', full_name='Anki.Vector.external_interface.CameraConfigResponse.fov_y', index=5, + number=6, type=2, cpp_type=6, label=1, + has_default_value=False, default_value=float(0), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='min_camera_exposure_time_ms', full_name='Anki.Vector.external_interface.CameraConfigResponse.min_camera_exposure_time_ms', index=6, + number=7, type=13, cpp_type=3, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='max_camera_exposure_time_ms', full_name='Anki.Vector.external_interface.CameraConfigResponse.max_camera_exposure_time_ms', index=7, + number=8, type=13, cpp_type=3, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='min_camera_gain', full_name='Anki.Vector.external_interface.CameraConfigResponse.min_camera_gain', index=8, + number=9, type=2, cpp_type=6, label=1, + has_default_value=False, default_value=float(0), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='max_camera_gain', full_name='Anki.Vector.external_interface.CameraConfigResponse.max_camera_gain', index=9, + number=10, type=2, cpp_type=6, label=1, + has_default_value=False, default_value=float(0), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=20258, + serialized_end=20518, +) + + +_SETCAMERASETTINGSREQUEST = _descriptor.Descriptor( + name='SetCameraSettingsRequest', + full_name='Anki.Vector.external_interface.SetCameraSettingsRequest', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='gain', full_name='Anki.Vector.external_interface.SetCameraSettingsRequest.gain', index=0, + number=1, type=2, cpp_type=6, label=1, + has_default_value=False, default_value=float(0), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='exposure_ms', full_name='Anki.Vector.external_interface.SetCameraSettingsRequest.exposure_ms', index=1, + number=2, type=13, cpp_type=3, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='enable_auto_exposure', full_name='Anki.Vector.external_interface.SetCameraSettingsRequest.enable_auto_exposure', index=2, + number=3, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=20520, + serialized_end=20611, +) + + +_SETCAMERASETTINGSRESPONSE = _descriptor.Descriptor( + name='SetCameraSettingsResponse', + full_name='Anki.Vector.external_interface.SetCameraSettingsResponse', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='status', full_name='Anki.Vector.external_interface.SetCameraSettingsResponse.status', index=0, + number=1, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='status_message', full_name='Anki.Vector.external_interface.SetCameraSettingsResponse.status_message', index=1, + number=2, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=20613, + serialized_end=20728, +) + + +_CAMERASETTINGSUPDATE = _descriptor.Descriptor( + name='CameraSettingsUpdate', + full_name='Anki.Vector.external_interface.CameraSettingsUpdate', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='gain', full_name='Anki.Vector.external_interface.CameraSettingsUpdate.gain', index=0, + number=1, type=2, cpp_type=6, label=1, + has_default_value=False, default_value=float(0), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='exposure_ms', full_name='Anki.Vector.external_interface.CameraSettingsUpdate.exposure_ms', index=1, + number=2, type=13, cpp_type=3, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='auto_exposure_enabled', full_name='Anki.Vector.external_interface.CameraSettingsUpdate.auto_exposure_enabled', index=2, + number=3, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=20730, + serialized_end=20818, ) @@ -7040,8 +7739,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=19197, - serialized_end=19347, + serialized_start=20821, + serialized_end=20971, ) @@ -7071,8 +7770,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=19349, - serialized_end=19440, + serialized_start=20973, + serialized_end=21064, ) _DRIVEWHEELSRESPONSE.fields_by_name['status'].message_type = anki__vector_dot_messaging_dot_response__status__pb2._RESPONSESTATUS @@ -7115,6 +7814,8 @@ _ROBOTSTATE.fields_by_name['gyro'].message_type = _GYRODATA _ROBOTSTATE.fields_by_name['prox_data'].message_type = _PROXDATA _ROBOTSTATE.fields_by_name['touch_data'].message_type = _TOUCHDATA +_UNEXPECTEDMOVEMENT.fields_by_name['movement_type'].enum_type = _UNEXPECTEDMOVEMENTTYPE +_UNEXPECTEDMOVEMENT.fields_by_name['movement_side'].enum_type = _UNEXPECTEDMOVEMENTSIDE _ROBOTOBSERVEDFACE.fields_by_name['pose'].message_type = _POSESTRUCT _ROBOTOBSERVEDFACE.fields_by_name['img_rect'].message_type = _CLADRECT _ROBOTOBSERVEDFACE.fields_by_name['expression'].enum_type = _FACIALEXPRESSION @@ -7130,6 +7831,8 @@ _ERASEENROLLEDFACEBYIDRESPONSE.fields_by_name['status'].message_type = anki__vector_dot_messaging_dot_response__status__pb2._RESPONSESTATUS _ERASEALLENROLLEDFACESRESPONSE.fields_by_name['status'].message_type = anki__vector_dot_messaging_dot_response__status__pb2._RESPONSESTATUS _SETFACETOENROLLRESPONSE.fields_by_name['status'].message_type = anki__vector_dot_messaging_dot_response__status__pb2._RESPONSESTATUS +_ENROLLFACERESPONSE.fields_by_name['status'].message_type = anki__vector_dot_messaging_dot_response__status__pb2._RESPONSESTATUS +_ENROLLFACERESPONSE.fields_by_name['result'].enum_type = _BEHAVIORRESULTS _DRIVEOFFCHARGERRESPONSE.fields_by_name['status'].message_type = anki__vector_dot_messaging_dot_response__status__pb2._RESPONSESTATUS _DRIVEOFFCHARGERRESPONSE.fields_by_name['result'].enum_type = _BEHAVIORRESULTS _DRIVEONCHARGERRESPONSE.fields_by_name['status'].message_type = anki__vector_dot_messaging_dot_response__status__pb2._RESPONSESTATUS @@ -7148,6 +7851,7 @@ _ACTIONRESULT.fields_by_name['code'].enum_type = _ACTIONRESULT_ACTIONRESULTCODE _ACTIONRESULT_ACTIONRESULTCODE.containing_type = _ACTIONRESULT _CANCELACTIONBYIDTAGRESPONSE.fields_by_name['status'].message_type = anki__vector_dot_messaging_dot_response__status__pb2._RESPONSESTATUS +_CANCELBEHAVIORRESPONSE.fields_by_name['status'].message_type = anki__vector_dot_messaging_dot_response__status__pb2._RESPONSESTATUS _GOTOPOSEREQUEST.fields_by_name['motion_prof'].message_type = _PATHMOTIONPROFILE _GOTOPOSERESPONSE.fields_by_name['status'].message_type = anki__vector_dot_messaging_dot_response__status__pb2._RESPONSESTATUS _GOTOPOSERESPONSE.fields_by_name['result'].message_type = _ACTIONRESULT @@ -7231,6 +7935,7 @@ _CAPTURESINGLEIMAGERESPONSE.fields_by_name['status'].message_type = anki__vector_dot_messaging_dot_response__status__pb2._RESPONSESTATUS _CAPTURESINGLEIMAGERESPONSE.fields_by_name['image_encoding'].enum_type = _IMAGECHUNK_IMAGEENCODING _SETEYECOLORRESPONSE.fields_by_name['status'].message_type = anki__vector_dot_messaging_dot_response__status__pb2._RESPONSESTATUS +_SETCAMERASETTINGSRESPONSE.fields_by_name['status'].message_type = anki__vector_dot_messaging_dot_response__status__pb2._RESPONSESTATUS _SDKINITIALIZATIONRESPONSE.fields_by_name['status'].message_type = anki__vector_dot_messaging_dot_response__status__pb2._RESPONSESTATUS DESCRIPTOR.message_types_by_name['KeepAlivePing'] = _KEEPALIVEPING DESCRIPTOR.message_types_by_name['AnimationTrigger'] = _ANIMATIONTRIGGER @@ -7265,6 +7970,9 @@ DESCRIPTOR.message_types_by_name['RobotState'] = _ROBOTSTATE DESCRIPTOR.message_types_by_name['CladPoint'] = _CLADPOINT DESCRIPTOR.message_types_by_name['CladRect'] = _CLADRECT +DESCRIPTOR.message_types_by_name['RobotObservedMotion'] = _ROBOTOBSERVEDMOTION +DESCRIPTOR.message_types_by_name['RobotErasedEnrolledFace'] = _ROBOTERASEDENROLLEDFACE +DESCRIPTOR.message_types_by_name['UnexpectedMovement'] = _UNEXPECTEDMOVEMENT DESCRIPTOR.message_types_by_name['RobotObservedFace'] = _ROBOTOBSERVEDFACE DESCRIPTOR.message_types_by_name['RobotChangedObservedFaceID'] = _ROBOTCHANGEDOBSERVEDFACEID DESCRIPTOR.message_types_by_name['FaceEnrollmentCompleted'] = _FACEENROLLMENTCOMPLETED @@ -7282,6 +7990,8 @@ DESCRIPTOR.message_types_by_name['EraseAllEnrolledFacesResponse'] = _ERASEALLENROLLEDFACESRESPONSE DESCRIPTOR.message_types_by_name['SetFaceToEnrollRequest'] = _SETFACETOENROLLREQUEST DESCRIPTOR.message_types_by_name['SetFaceToEnrollResponse'] = _SETFACETOENROLLRESPONSE +DESCRIPTOR.message_types_by_name['EnrollFaceRequest'] = _ENROLLFACEREQUEST +DESCRIPTOR.message_types_by_name['EnrollFaceResponse'] = _ENROLLFACERESPONSE DESCRIPTOR.message_types_by_name['DriveOffChargerRequest'] = _DRIVEOFFCHARGERREQUEST DESCRIPTOR.message_types_by_name['DriveOffChargerResponse'] = _DRIVEOFFCHARGERRESPONSE DESCRIPTOR.message_types_by_name['DriveOnChargerRequest'] = _DRIVEONCHARGERREQUEST @@ -7311,6 +8021,8 @@ DESCRIPTOR.message_types_by_name['ActionResult'] = _ACTIONRESULT DESCRIPTOR.message_types_by_name['CancelActionByIdTagRequest'] = _CANCELACTIONBYIDTAGREQUEST DESCRIPTOR.message_types_by_name['CancelActionByIdTagResponse'] = _CANCELACTIONBYIDTAGRESPONSE +DESCRIPTOR.message_types_by_name['CancelBehaviorRequest'] = _CANCELBEHAVIORREQUEST +DESCRIPTOR.message_types_by_name['CancelBehaviorResponse'] = _CANCELBEHAVIORRESPONSE DESCRIPTOR.message_types_by_name['GoToPoseRequest'] = _GOTOPOSEREQUEST DESCRIPTOR.message_types_by_name['GoToPoseResponse'] = _GOTOPOSERESPONSE DESCRIPTOR.message_types_by_name['DockWithCubeRequest'] = _DOCKWITHCUBEREQUEST @@ -7380,9 +8092,16 @@ DESCRIPTOR.message_types_by_name['CaptureSingleImageResponse'] = _CAPTURESINGLEIMAGERESPONSE DESCRIPTOR.message_types_by_name['SetEyeColorRequest'] = _SETEYECOLORREQUEST DESCRIPTOR.message_types_by_name['SetEyeColorResponse'] = _SETEYECOLORRESPONSE +DESCRIPTOR.message_types_by_name['CameraConfigRequest'] = _CAMERACONFIGREQUEST +DESCRIPTOR.message_types_by_name['CameraConfigResponse'] = _CAMERACONFIGRESPONSE +DESCRIPTOR.message_types_by_name['SetCameraSettingsRequest'] = _SETCAMERASETTINGSREQUEST +DESCRIPTOR.message_types_by_name['SetCameraSettingsResponse'] = _SETCAMERASETTINGSRESPONSE +DESCRIPTOR.message_types_by_name['CameraSettingsUpdate'] = _CAMERASETTINGSUPDATE DESCRIPTOR.message_types_by_name['SDKInitializationRequest'] = _SDKINITIALIZATIONREQUEST DESCRIPTOR.message_types_by_name['SDKInitializationResponse'] = _SDKINITIALIZATIONRESPONSE DESCRIPTOR.enum_types_by_name['RobotStatus'] = _ROBOTSTATUS +DESCRIPTOR.enum_types_by_name['UnexpectedMovementType'] = _UNEXPECTEDMOVEMENTTYPE +DESCRIPTOR.enum_types_by_name['UnexpectedMovementSide'] = _UNEXPECTEDMOVEMENTSIDE DESCRIPTOR.enum_types_by_name['FacialExpression'] = _FACIALEXPRESSION DESCRIPTOR.enum_types_by_name['FaceEnrollmentResult'] = _FACEENROLLMENTRESULT DESCRIPTOR.enum_types_by_name['BehaviorResults'] = _BEHAVIORRESULTS @@ -7625,6 +8344,27 @@ )) _sym_db.RegisterMessage(CladRect) +RobotObservedMotion = _reflection.GeneratedProtocolMessageType('RobotObservedMotion', (_message.Message,), dict( + DESCRIPTOR = _ROBOTOBSERVEDMOTION, + __module__ = 'anki_vector.messaging.messages_pb2' + # @@protoc_insertion_point(class_scope:Anki.Vector.external_interface.RobotObservedMotion) + )) +_sym_db.RegisterMessage(RobotObservedMotion) + +RobotErasedEnrolledFace = _reflection.GeneratedProtocolMessageType('RobotErasedEnrolledFace', (_message.Message,), dict( + DESCRIPTOR = _ROBOTERASEDENROLLEDFACE, + __module__ = 'anki_vector.messaging.messages_pb2' + # @@protoc_insertion_point(class_scope:Anki.Vector.external_interface.RobotErasedEnrolledFace) + )) +_sym_db.RegisterMessage(RobotErasedEnrolledFace) + +UnexpectedMovement = _reflection.GeneratedProtocolMessageType('UnexpectedMovement', (_message.Message,), dict( + DESCRIPTOR = _UNEXPECTEDMOVEMENT, + __module__ = 'anki_vector.messaging.messages_pb2' + # @@protoc_insertion_point(class_scope:Anki.Vector.external_interface.UnexpectedMovement) + )) +_sym_db.RegisterMessage(UnexpectedMovement) + RobotObservedFace = _reflection.GeneratedProtocolMessageType('RobotObservedFace', (_message.Message,), dict( DESCRIPTOR = _ROBOTOBSERVEDFACE, __module__ = 'anki_vector.messaging.messages_pb2' @@ -7744,6 +8484,20 @@ )) _sym_db.RegisterMessage(SetFaceToEnrollResponse) +EnrollFaceRequest = _reflection.GeneratedProtocolMessageType('EnrollFaceRequest', (_message.Message,), dict( + DESCRIPTOR = _ENROLLFACEREQUEST, + __module__ = 'anki_vector.messaging.messages_pb2' + # @@protoc_insertion_point(class_scope:Anki.Vector.external_interface.EnrollFaceRequest) + )) +_sym_db.RegisterMessage(EnrollFaceRequest) + +EnrollFaceResponse = _reflection.GeneratedProtocolMessageType('EnrollFaceResponse', (_message.Message,), dict( + DESCRIPTOR = _ENROLLFACERESPONSE, + __module__ = 'anki_vector.messaging.messages_pb2' + # @@protoc_insertion_point(class_scope:Anki.Vector.external_interface.EnrollFaceResponse) + )) +_sym_db.RegisterMessage(EnrollFaceResponse) + DriveOffChargerRequest = _reflection.GeneratedProtocolMessageType('DriveOffChargerRequest', (_message.Message,), dict( DESCRIPTOR = _DRIVEOFFCHARGERREQUEST, __module__ = 'anki_vector.messaging.messages_pb2' @@ -7947,6 +8701,20 @@ )) _sym_db.RegisterMessage(CancelActionByIdTagResponse) +CancelBehaviorRequest = _reflection.GeneratedProtocolMessageType('CancelBehaviorRequest', (_message.Message,), dict( + DESCRIPTOR = _CANCELBEHAVIORREQUEST, + __module__ = 'anki_vector.messaging.messages_pb2' + # @@protoc_insertion_point(class_scope:Anki.Vector.external_interface.CancelBehaviorRequest) + )) +_sym_db.RegisterMessage(CancelBehaviorRequest) + +CancelBehaviorResponse = _reflection.GeneratedProtocolMessageType('CancelBehaviorResponse', (_message.Message,), dict( + DESCRIPTOR = _CANCELBEHAVIORRESPONSE, + __module__ = 'anki_vector.messaging.messages_pb2' + # @@protoc_insertion_point(class_scope:Anki.Vector.external_interface.CancelBehaviorResponse) + )) +_sym_db.RegisterMessage(CancelBehaviorResponse) + GoToPoseRequest = _reflection.GeneratedProtocolMessageType('GoToPoseRequest', (_message.Message,), dict( DESCRIPTOR = _GOTOPOSEREQUEST, __module__ = 'anki_vector.messaging.messages_pb2' @@ -8430,6 +9198,41 @@ )) _sym_db.RegisterMessage(SetEyeColorResponse) +CameraConfigRequest = _reflection.GeneratedProtocolMessageType('CameraConfigRequest', (_message.Message,), dict( + DESCRIPTOR = _CAMERACONFIGREQUEST, + __module__ = 'anki_vector.messaging.messages_pb2' + # @@protoc_insertion_point(class_scope:Anki.Vector.external_interface.CameraConfigRequest) + )) +_sym_db.RegisterMessage(CameraConfigRequest) + +CameraConfigResponse = _reflection.GeneratedProtocolMessageType('CameraConfigResponse', (_message.Message,), dict( + DESCRIPTOR = _CAMERACONFIGRESPONSE, + __module__ = 'anki_vector.messaging.messages_pb2' + # @@protoc_insertion_point(class_scope:Anki.Vector.external_interface.CameraConfigResponse) + )) +_sym_db.RegisterMessage(CameraConfigResponse) + +SetCameraSettingsRequest = _reflection.GeneratedProtocolMessageType('SetCameraSettingsRequest', (_message.Message,), dict( + DESCRIPTOR = _SETCAMERASETTINGSREQUEST, + __module__ = 'anki_vector.messaging.messages_pb2' + # @@protoc_insertion_point(class_scope:Anki.Vector.external_interface.SetCameraSettingsRequest) + )) +_sym_db.RegisterMessage(SetCameraSettingsRequest) + +SetCameraSettingsResponse = _reflection.GeneratedProtocolMessageType('SetCameraSettingsResponse', (_message.Message,), dict( + DESCRIPTOR = _SETCAMERASETTINGSRESPONSE, + __module__ = 'anki_vector.messaging.messages_pb2' + # @@protoc_insertion_point(class_scope:Anki.Vector.external_interface.SetCameraSettingsResponse) + )) +_sym_db.RegisterMessage(SetCameraSettingsResponse) + +CameraSettingsUpdate = _reflection.GeneratedProtocolMessageType('CameraSettingsUpdate', (_message.Message,), dict( + DESCRIPTOR = _CAMERASETTINGSUPDATE, + __module__ = 'anki_vector.messaging.messages_pb2' + # @@protoc_insertion_point(class_scope:Anki.Vector.external_interface.CameraSettingsUpdate) + )) +_sym_db.RegisterMessage(CameraSettingsUpdate) + SDKInitializationRequest = _reflection.GeneratedProtocolMessageType('SDKInitializationRequest', (_message.Message,), dict( DESCRIPTOR = _SDKINITIALIZATIONREQUEST, __module__ = 'anki_vector.messaging.messages_pb2' diff --git a/anki_vector/messaging/settings.proto b/anki_vector/messaging/settings.proto index 36fc97e..c7342ae 100644 --- a/anki_vector/messaging/settings.proto +++ b/anki_vector/messaging/settings.proto @@ -18,4 +18,159 @@ syntax = "proto3"; package Anki.Vector.external_interface; +import "anki_vector/messaging/response_status.proto"; +// Updated by wayne@codaris.com +message RobotSettingsConfig { + bool clock_24_hour = 1; + EyeColor eye_color = 2; + string default_location = 3; + bool dist_is_metric = 4; + string locale = 5; + Volume master_volume = 6; + bool temp_is_fahrenheit = 7; + string time_zone = 8; + ButtonWakeWord button_wakeword = 9; +} + + +message AccountSettingsConfig { + oneof oneof_data_collection { + bool data_collection = 1; + } + + oneof oneof_app_locale { + string app_locale = 2; + } +} + +message UserEntitlementsConfig { + oneof oneof_kickstarter_eyes { + bool kickstarter_eyes = 1; + } +} + +message Jdoc { + uint64 doc_version = 1; + uint64 fmt_version = 2; + string client_metadata = 3; + string json_doc = 4; +} + +message NamedJdoc { + JdocType jdoc_type = 1; + Jdoc doc = 2; +} + +message PullJdocsRequest { + repeated JdocType jdoc_types = 1; +} + +message PullJdocsResponse { + ResponseStatus status = 1; + repeated NamedJdoc named_jdocs = 2; +} + +message UpdateSettingsRequest { + RobotSettingsConfig settings = 1; +} + +message UpdateSettingsResponse { + ResponseStatus status = 1; + ResultCode code = 2; + Jdoc doc = 3; +} + +message UpdateAccountSettingsRequest { + AccountSettingsConfig account_settings = 1; +} + +message UpdateAccountSettingsResponse { + ResponseStatus status = 1; + ResultCode code = 2; + Jdoc doc = 3; +} + +message UpdateUserEntitlementsRequest { + UserEntitlementsConfig user_entitlements = 1; +} + +message UpdateUserEntitlementsResponse { + ResponseStatus status = 1; + ResultCode code = 2; + Jdoc doc = 3; +} + +message JdocsChanged { + repeated JdocType jdoc_types = 1; +} + +enum ApiVersion { + INVALID = 0; + LATEST = 1; +} + +enum Volume { + MUTE = 0; + LOW = 1; + MEDIUM_LOW = 2; + MEDIUM = 3; + MEDIUM_HIGH = 4; + HIGH = 5; +} + +enum JdocType { + ROBOT_SETTINGS = 0; + ROBOT_LIFETIME_STATS = 1; + ACCOUNT_SETTINGS = 2; + USER_ENTITLEMENTS = 3; +} + +enum JdocResolveMethod { + PUSH_TO_CLOUD = 0; + PULL_FROM_CLOUD = 1; +} + +// RobotSetting enum values are not in all caps for historical reasons. +// Changing that now would involve a format migration because we're now +// saving robot settings jdocs in the cloud. +enum RobotSetting { + clock_24_hour = 0; + eye_color = 1; + default_location = 2; + dist_is_metric = 3; + locale = 4; + master_volume = 5; + temp_is_fahrenheit = 6; + time_zone = 7; + button_wakeword = 8; +} + +enum EyeColor { + TIP_OVER_TEAL = 0; + OVERFIT_ORANGE = 1; + UNCANNY_YELLOW = 2; + NON_LINEAR_LIME = 3; + SINGULARITY_SAPPHIRE = 4; + FALSE_POSITIVE_PURPLE = 5; + CONFUSION_MATRIX_GREEN = 6; +} + +enum ButtonWakeWord { + BUTTON_WAKEWORD_HEY_VECTOR = 0; + BUTTON_WAKEWORD_ALEXA = 1; +} + +enum AccountSetting { + DATA_COLLECTION = 0; + APP_LOCALE = 1; +} + +enum UserEntitlement { + KICKSTARTER_EYES = 0; +} + +enum ResultCode { + SETTINGS_ACCEPTED = 0; + ERROR_UPDATE_IN_PROGRESS = 1; +} diff --git a/anki_vector/messaging/shared.proto b/anki_vector/messaging/shared.proto index d193d80..316f9eb 100644 --- a/anki_vector/messaging/shared.proto +++ b/anki_vector/messaging/shared.proto @@ -20,6 +20,7 @@ package Anki.Vector.external_interface; import "anki_vector/messaging/behavior.proto"; import "anki_vector/messaging/cube.proto"; +import "anki_vector/messaging/alexa.proto"; import "anki_vector/messaging/messages.proto"; import "anki_vector/messaging/settings.proto"; import "anki_vector/messaging/extensions.proto"; @@ -49,6 +50,7 @@ message Event { oneof event_type { TimeStampedStatus time_stamped_status = 1; WakeWord wake_word = 3; + AttentionTransfer attention_transfer = 4; RobotObservedFace robot_observed_face = 5; RobotChangedObservedFaceID robot_changed_observed_face_id = 6; ObjectEvent object_event = 7; @@ -56,12 +58,19 @@ message Event { PhotoTaken photo_taken = 9; RobotState robot_state = 10; CubeBattery cube_battery = 11; - // Used by Vector to verify the connection is still alive. - KeepAlivePing keep_alive = 12; + KeepAlivePing keep_alive = 12; // Used by Vector to verify the connection is still alive. ConnectionResponse connection_response = 13; + JdocsChanged jdocs_changed = 14; + AlexaAuthEvent alexa_auth_event = 15; MirrorModeDisabled mirror_mode_disabled = 16; VisionModesAutoDisabled vision_modes_auto_disabled = 17; + CheckUpdateStatusResponse check_update_status_response = 18; UserIntent user_intent = 19; + RobotObservedMotion robot_observed_motion = 20; + RobotErasedEnrolledFace robot_erased_enrolled_face = 21; + RobotRenamedEnrolledFace robot_renamed_enrolled_face = 22; + CameraSettingsUpdate camera_settings_update = 23; + UnexpectedMovement unexpected_movement = 24; } } diff --git a/anki_vector/messaging/shared_pb2.py b/anki_vector/messaging/shared_pb2.py index a713482..f175481 100644 --- a/anki_vector/messaging/shared_pb2.py +++ b/anki_vector/messaging/shared_pb2.py @@ -25,7 +25,7 @@ name='anki_vector/messaging/shared.proto', package='Anki.Vector.external_interface', syntax='proto3', - serialized_pb=_b('\n\"anki_vector/messaging/shared.proto\x12\x1e\x41nki.Vector.external_interface\x1a$anki_vector/messaging/behavior.proto\x1a anki_vector/messaging/cube.proto\x1a$anki_vector/messaging/messages.proto\x1a$anki_vector/messaging/settings.proto\x1a&anki_vector/messaging/extensions.proto\x1a+anki_vector/messaging/response_status.proto\"J\n\x16ProtocolVersionRequest\x12\x16\n\x0e\x63lient_version\x18\x01 \x01(\x03\x12\x18\n\x10min_host_version\x18\x02 \x01(\x03\"\xa7\x01\n\x17ProtocolVersionResponse\x12N\n\x06result\x18\x01 \x01(\x0e\x32>.Anki.Vector.external_interface.ProtocolVersionResponse.Result\x12\x14\n\x0chost_version\x18\x02 \x01(\x03\"&\n\x06Result\x12\x0f\n\x0bUNSUPPORTED\x10\x00\x12\x0b\n\x07SUCCESS\x10\x01\"h\n\x12\x43onnectionResponse\x12>\n\x06status\x18\x01 \x01(\x0b\x32..Anki.Vector.external_interface.ResponseStatus\x12\x12\n\nis_primary\x18\x02 \x01(\x08\"\xc9\x08\n\x05\x45vent\x12P\n\x13time_stamped_status\x18\x01 \x01(\x0b\x32\x31.Anki.Vector.external_interface.TimeStampedStatusH\x00\x12=\n\twake_word\x18\x03 \x01(\x0b\x32(.Anki.Vector.external_interface.WakeWordH\x00\x12P\n\x13robot_observed_face\x18\x05 \x01(\x0b\x32\x31.Anki.Vector.external_interface.RobotObservedFaceH\x00\x12\x64\n\x1erobot_changed_observed_face_id\x18\x06 \x01(\x0b\x32:.Anki.Vector.external_interface.RobotChangedObservedFaceIDH\x00\x12\x43\n\x0cobject_event\x18\x07 \x01(\x0b\x32+.Anki.Vector.external_interface.ObjectEventH\x00\x12K\n\x10stimulation_info\x18\x08 \x01(\x0b\x32/.Anki.Vector.external_interface.StimulationInfoH\x00\x12\x41\n\x0bphoto_taken\x18\t \x01(\x0b\x32*.Anki.Vector.external_interface.PhotoTakenH\x00\x12\x41\n\x0brobot_state\x18\n \x01(\x0b\x32*.Anki.Vector.external_interface.RobotStateH\x00\x12\x43\n\x0c\x63ube_battery\x18\x0b \x01(\x0b\x32+.Anki.Vector.external_interface.CubeBatteryH\x00\x12\x43\n\nkeep_alive\x18\x0c \x01(\x0b\x32-.Anki.Vector.external_interface.KeepAlivePingH\x00\x12Q\n\x13\x63onnection_response\x18\r \x01(\x0b\x32\x32.Anki.Vector.external_interface.ConnectionResponseH\x00\x12R\n\x14mirror_mode_disabled\x18\x10 \x01(\x0b\x32\x32.Anki.Vector.external_interface.MirrorModeDisabledH\x00\x12]\n\x1avision_modes_auto_disabled\x18\x11 \x01(\x0b\x32\x37.Anki.Vector.external_interface.VisionModesAutoDisabledH\x00\x12\x41\n\x0buser_intent\x18\x13 \x01(\x0b\x32*.Anki.Vector.external_interface.UserIntentH\x00\x42\x0c\n\nevent_type\"\x1a\n\nFilterList\x12\x0c\n\x04list\x18\x01 \x03(\t\"\xb6\x01\n\x0c\x45ventRequest\x12@\n\nwhite_list\x18\x01 \x01(\x0b\x32*.Anki.Vector.external_interface.FilterListH\x00\x12@\n\nblack_list\x18\x02 \x01(\x0b\x32*.Anki.Vector.external_interface.FilterListH\x00\x12\x15\n\rconnection_id\x18\x03 \x01(\tB\x0b\n\tlist_type\"\x8b\x01\n\rEventResponse\x12>\n\x06status\x18\x01 \x01(\x0b\x32..Anki.Vector.external_interface.ResponseStatus\x12\x34\n\x05\x65vent\x18\x02 \x01(\x0b\x32%.Anki.Vector.external_interface.Event:\x04\x80\xa6\x1d\x01\"I\n\x19UserAuthenticationRequest\x12\x17\n\x0fuser_session_id\x18\x01 \x01(\x0c\x12\x13\n\x0b\x63lient_name\x18\x02 \x01(\x0c\"\xf0\x01\n\x1aUserAuthenticationResponse\x12>\n\x06status\x18\x01 \x01(\x0b\x32..Anki.Vector.external_interface.ResponseStatus\x12M\n\x04\x63ode\x18\x02 \x01(\x0e\x32?.Anki.Vector.external_interface.UserAuthenticationResponse.Code\x12\x19\n\x11\x63lient_token_guid\x18\x03 \x01(\x0c\"(\n\x04\x43ode\x12\x10\n\x0cUNAUTHORIZED\x10\x00\x12\x0e\n\nAUTHORIZED\x10\x01\x62\x06proto3') + serialized_pb=_b('\n\"anki_vector/messaging/shared.proto\x12\x1e\x41nki.Vector.external_interface\x1a$anki_vector/messaging/behavior.proto\x1a anki_vector/messaging/cube.proto\x1a$anki_vector/messaging/messages.proto\x1a$anki_vector/messaging/settings.proto\x1a&anki_vector/messaging/extensions.proto\x1a+anki_vector/messaging/response_status.proto\"J\n\x16ProtocolVersionRequest\x12\x16\n\x0e\x63lient_version\x18\x01 \x01(\x03\x12\x18\n\x10min_host_version\x18\x02 \x01(\x03\"\xa7\x01\n\x17ProtocolVersionResponse\x12N\n\x06result\x18\x01 \x01(\x0e\x32>.Anki.Vector.external_interface.ProtocolVersionResponse.Result\x12\x14\n\x0chost_version\x18\x02 \x01(\x03\"&\n\x06Result\x12\x0f\n\x0bUNSUPPORTED\x10\x00\x12\x0b\n\x07SUCCESS\x10\x01\"h\n\x12\x43onnectionResponse\x12>\n\x06status\x18\x01 \x01(\x0b\x32..Anki.Vector.external_interface.ResponseStatus\x12\x12\n\nis_primary\x18\x02 \x01(\x08\"\x8a\x0c\n\x05\x45vent\x12P\n\x13time_stamped_status\x18\x01 \x01(\x0b\x32\x31.Anki.Vector.external_interface.TimeStampedStatusH\x00\x12=\n\twake_word\x18\x03 \x01(\x0b\x32(.Anki.Vector.external_interface.WakeWordH\x00\x12P\n\x13robot_observed_face\x18\x05 \x01(\x0b\x32\x31.Anki.Vector.external_interface.RobotObservedFaceH\x00\x12\x64\n\x1erobot_changed_observed_face_id\x18\x06 \x01(\x0b\x32:.Anki.Vector.external_interface.RobotChangedObservedFaceIDH\x00\x12\x43\n\x0cobject_event\x18\x07 \x01(\x0b\x32+.Anki.Vector.external_interface.ObjectEventH\x00\x12K\n\x10stimulation_info\x18\x08 \x01(\x0b\x32/.Anki.Vector.external_interface.StimulationInfoH\x00\x12\x41\n\x0bphoto_taken\x18\t \x01(\x0b\x32*.Anki.Vector.external_interface.PhotoTakenH\x00\x12\x41\n\x0brobot_state\x18\n \x01(\x0b\x32*.Anki.Vector.external_interface.RobotStateH\x00\x12\x43\n\x0c\x63ube_battery\x18\x0b \x01(\x0b\x32+.Anki.Vector.external_interface.CubeBatteryH\x00\x12\x43\n\nkeep_alive\x18\x0c \x01(\x0b\x32-.Anki.Vector.external_interface.KeepAlivePingH\x00\x12Q\n\x13\x63onnection_response\x18\r \x01(\x0b\x32\x32.Anki.Vector.external_interface.ConnectionResponseH\x00\x12R\n\x14mirror_mode_disabled\x18\x10 \x01(\x0b\x32\x32.Anki.Vector.external_interface.MirrorModeDisabledH\x00\x12]\n\x1avision_modes_auto_disabled\x18\x11 \x01(\x0b\x32\x37.Anki.Vector.external_interface.VisionModesAutoDisabledH\x00\x12\x41\n\x0buser_intent\x18\x13 \x01(\x0b\x32*.Anki.Vector.external_interface.UserIntentH\x00\x12T\n\x15robot_observed_motion\x18\x14 \x01(\x0b\x32\x33.Anki.Vector.external_interface.RobotObservedMotionH\x00\x12]\n\x1arobot_erased_enrolled_face\x18\x15 \x01(\x0b\x32\x37.Anki.Vector.external_interface.RobotErasedEnrolledFaceH\x00\x12_\n\x1brobot_renamed_enrolled_face\x18\x16 \x01(\x0b\x32\x38.Anki.Vector.external_interface.RobotRenamedEnrolledFaceH\x00\x12V\n\x16\x63\x61mera_settings_update\x18\x17 \x01(\x0b\x32\x34.Anki.Vector.external_interface.CameraSettingsUpdateH\x00\x12Q\n\x13unexpected_movement\x18\x18 \x01(\x0b\x32\x32.Anki.Vector.external_interface.UnexpectedMovementH\x00\x42\x0c\n\nevent_type\"\x1a\n\nFilterList\x12\x0c\n\x04list\x18\x01 \x03(\t\"\xb6\x01\n\x0c\x45ventRequest\x12@\n\nwhite_list\x18\x01 \x01(\x0b\x32*.Anki.Vector.external_interface.FilterListH\x00\x12@\n\nblack_list\x18\x02 \x01(\x0b\x32*.Anki.Vector.external_interface.FilterListH\x00\x12\x15\n\rconnection_id\x18\x03 \x01(\tB\x0b\n\tlist_type\"\x8b\x01\n\rEventResponse\x12>\n\x06status\x18\x01 \x01(\x0b\x32..Anki.Vector.external_interface.ResponseStatus\x12\x34\n\x05\x65vent\x18\x02 \x01(\x0b\x32%.Anki.Vector.external_interface.Event:\x04\x80\xa6\x1d\x01\"I\n\x19UserAuthenticationRequest\x12\x17\n\x0fuser_session_id\x18\x01 \x01(\x0c\x12\x13\n\x0b\x63lient_name\x18\x02 \x01(\x0c\"\xf0\x01\n\x1aUserAuthenticationResponse\x12>\n\x06status\x18\x01 \x01(\x0b\x32..Anki.Vector.external_interface.ResponseStatus\x12M\n\x04\x63ode\x18\x02 \x01(\x0e\x32?.Anki.Vector.external_interface.UserAuthenticationResponse.Code\x12\x19\n\x11\x63lient_token_guid\x18\x03 \x01(\x0c\"(\n\x04\x43ode\x12\x10\n\x0cUNAUTHORIZED\x10\x00\x12\x0e\n\nAUTHORIZED\x10\x01\x62\x06proto3') , dependencies=[anki__vector_dot_messaging_dot_behavior__pb2.DESCRIPTOR,anki__vector_dot_messaging_dot_cube__pb2.DESCRIPTOR,anki__vector_dot_messaging_dot_messages__pb2.DESCRIPTOR,anki__vector_dot_messaging_dot_settings__pb2.DESCRIPTOR,anki__vector_dot_messaging_dot_extensions__pb2.DESCRIPTOR,anki__vector_dot_messaging_dot_response__status__pb2.DESCRIPTOR,]) @@ -70,8 +70,8 @@ ], containing_type=None, options=None, - serialized_start=2386, - serialized_end=2426, + serialized_start=2835, + serialized_end=2875, ) _sym_db.RegisterEnumDescriptor(_USERAUTHENTICATIONRESPONSE_CODE) @@ -296,6 +296,41 @@ message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='robot_observed_motion', full_name='Anki.Vector.external_interface.Event.robot_observed_motion', index=14, + number=20, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='robot_erased_enrolled_face', full_name='Anki.Vector.external_interface.Event.robot_erased_enrolled_face', index=15, + number=21, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='robot_renamed_enrolled_face', full_name='Anki.Vector.external_interface.Event.robot_renamed_enrolled_face', index=16, + number=22, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='camera_settings_update', full_name='Anki.Vector.external_interface.Event.camera_settings_update', index=17, + number=23, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='unexpected_movement', full_name='Anki.Vector.external_interface.Event.unexpected_movement', index=18, + number=24, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None, file=DESCRIPTOR), ], extensions=[ ], @@ -312,7 +347,7 @@ index=0, containing_type=None, fields=[]), ], serialized_start=656, - serialized_end=1753, + serialized_end=2202, ) @@ -342,8 +377,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=1755, - serialized_end=1781, + serialized_start=2204, + serialized_end=2230, ) @@ -390,8 +425,8 @@ name='list_type', full_name='Anki.Vector.external_interface.EventRequest.list_type', index=0, containing_type=None, fields=[]), ], - serialized_start=1784, - serialized_end=1966, + serialized_start=2233, + serialized_end=2415, ) @@ -428,8 +463,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=1969, - serialized_end=2108, + serialized_start=2418, + serialized_end=2557, ) @@ -466,8 +501,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=2110, - serialized_end=2183, + serialized_start=2559, + serialized_end=2632, ) @@ -512,8 +547,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=2186, - serialized_end=2426, + serialized_start=2635, + serialized_end=2875, ) _PROTOCOLVERSIONRESPONSE.fields_by_name['result'].enum_type = _PROTOCOLVERSIONRESPONSE_RESULT @@ -533,6 +568,11 @@ _EVENT.fields_by_name['mirror_mode_disabled'].message_type = anki__vector_dot_messaging_dot_messages__pb2._MIRRORMODEDISABLED _EVENT.fields_by_name['vision_modes_auto_disabled'].message_type = anki__vector_dot_messaging_dot_messages__pb2._VISIONMODESAUTODISABLED _EVENT.fields_by_name['user_intent'].message_type = anki__vector_dot_messaging_dot_messages__pb2._USERINTENT +_EVENT.fields_by_name['robot_observed_motion'].message_type = anki__vector_dot_messaging_dot_messages__pb2._ROBOTOBSERVEDMOTION +_EVENT.fields_by_name['robot_erased_enrolled_face'].message_type = anki__vector_dot_messaging_dot_messages__pb2._ROBOTERASEDENROLLEDFACE +_EVENT.fields_by_name['robot_renamed_enrolled_face'].message_type = anki__vector_dot_messaging_dot_messages__pb2._ROBOTRENAMEDENROLLEDFACE +_EVENT.fields_by_name['camera_settings_update'].message_type = anki__vector_dot_messaging_dot_messages__pb2._CAMERASETTINGSUPDATE +_EVENT.fields_by_name['unexpected_movement'].message_type = anki__vector_dot_messaging_dot_messages__pb2._UNEXPECTEDMOVEMENT _EVENT.oneofs_by_name['event_type'].fields.append( _EVENT.fields_by_name['time_stamped_status']) _EVENT.fields_by_name['time_stamped_status'].containing_oneof = _EVENT.oneofs_by_name['event_type'] @@ -575,6 +615,21 @@ _EVENT.oneofs_by_name['event_type'].fields.append( _EVENT.fields_by_name['user_intent']) _EVENT.fields_by_name['user_intent'].containing_oneof = _EVENT.oneofs_by_name['event_type'] +_EVENT.oneofs_by_name['event_type'].fields.append( + _EVENT.fields_by_name['robot_observed_motion']) +_EVENT.fields_by_name['robot_observed_motion'].containing_oneof = _EVENT.oneofs_by_name['event_type'] +_EVENT.oneofs_by_name['event_type'].fields.append( + _EVENT.fields_by_name['robot_erased_enrolled_face']) +_EVENT.fields_by_name['robot_erased_enrolled_face'].containing_oneof = _EVENT.oneofs_by_name['event_type'] +_EVENT.oneofs_by_name['event_type'].fields.append( + _EVENT.fields_by_name['robot_renamed_enrolled_face']) +_EVENT.fields_by_name['robot_renamed_enrolled_face'].containing_oneof = _EVENT.oneofs_by_name['event_type'] +_EVENT.oneofs_by_name['event_type'].fields.append( + _EVENT.fields_by_name['camera_settings_update']) +_EVENT.fields_by_name['camera_settings_update'].containing_oneof = _EVENT.oneofs_by_name['event_type'] +_EVENT.oneofs_by_name['event_type'].fields.append( + _EVENT.fields_by_name['unexpected_movement']) +_EVENT.fields_by_name['unexpected_movement'].containing_oneof = _EVENT.oneofs_by_name['event_type'] _EVENTREQUEST.fields_by_name['white_list'].message_type = _FILTERLIST _EVENTREQUEST.fields_by_name['black_list'].message_type = _FILTERLIST _EVENTREQUEST.oneofs_by_name['list_type'].fields.append( diff --git a/anki_vector/objects.py b/anki_vector/objects.py index cf44929..43bf963 100644 --- a/anki_vector/objects.py +++ b/anki_vector/objects.py @@ -207,7 +207,7 @@ def __init__(self, obj, move_duration: float): class ObservableObject(util.Component): - """Represents any object Vector can see in the world.""" + """The base type for anything Vector can see.""" visibility_timeout = OBJECT_VISIBILITY_TIMEOUT @@ -324,6 +324,18 @@ def is_visible(self) -> bool: def _repr_values(self): # pylint: disable=no-self-use return '' + def _dispatch_observed_event(self, image_rect): + # Override in subclass if there is a specific event for that type + self.conn.run_soon(self._robot.events.dispatch_event(EvtObjectObserved(self, image_rect, self._pose), Events.object_observed)) + + def _dispatch_appeared_event(self, image_rect): + # Override in subclass if there is a specific event for that type + self.conn.run_soon(self._robot.events.dispatch_event(EvtObjectAppeared(self, image_rect, self._pose), Events.object_appeared)) + + def _dispatch_disappeared_event(self): + # Override in subclass if there is a specific event for that type + self.conn.run_soon(self._robot.events.dispatch_event(EvtObjectDisappeared(self), Events.object_disappeared)) + def _reset_observed_timeout_handler(self): if self._observed_timeout_handler is not None: self._observed_timeout_handler.cancel() @@ -333,7 +345,7 @@ def _observed_timeout(self): # Triggered when the element is no longer considered "visible". # i.e. visibility_timeout seconds after the last observed event. self._is_visible = False - self.conn.run_soon(self._robot.events.dispatch_event(EvtObjectDisappeared(self), Events.object_disappeared)) + self._dispatch_disappeared_event() def _on_observed(self, pose: util.Pose, image_rect: util.ImageRect, robot_timestamp: int): # Called from subclasses on their corresponding observed messages. @@ -347,10 +359,10 @@ def _on_observed(self, pose: util.Pose, image_rect: util.ImageRect, robot_timest self._last_observed_image_rect = image_rect self._pose = pose self._reset_observed_timeout_handler() - self.conn.run_soon(self._robot.events.dispatch_event(EvtObjectObserved(self, image_rect, pose), Events.object_observed)) + self._dispatch_observed_event(image_rect) if newly_visible: - self.conn.run_soon(self._robot.events.dispatch_event(EvtObjectAppeared(self, image_rect, pose), Events.object_appeared)) + self._dispatch_appeared_event(image_rect) #: LIGHT_CUBE_1_TYPE's markers look like 2 concentric circles with lines and gaps. @@ -481,6 +493,7 @@ def teardown(self): self.robot.events.unsubscribe(self._on_object_connection_lost, Events.cube_connection_lost) + # TODO: add return type hint @connection.on_connection_thread() async def set_light_corners(self, light1: lights.Light, diff --git a/anki_vector/robot.py b/anki_vector/robot.py index 77ccae8..4aaadec 100755 --- a/anki_vector/robot.py +++ b/anki_vector/robot.py @@ -694,6 +694,17 @@ def connect(self, timeout: int = 10) -> None: events.Events.robot_state, _on_connection_thread=True) + # get the camera configuration from the robot + response = self._camera.get_camera_config() + if isinstance(response, concurrent.futures.Future): + response = response.result() + self._camera.set_config(response) + + # Subscribe to a callback for camera exposure settings + self.events.subscribe(self._camera.update_state, + events.Events.camera_settings_update, + _on_connection_thread=True) + # access the pose to prove it has gotten back from the event stream once try: if not self.pose: diff --git a/anki_vector/version.py b/anki_vector/version.py index 252ecf8..63318c0 100644 --- a/anki_vector/version.py +++ b/anki_vector/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.6.1.dev0" +__version__ = "0.7.2.dev0" diff --git a/anki_vector/vision.py b/anki_vector/vision.py index d37ef6a..84eb27e 100644 --- a/anki_vector/vision.py +++ b/anki_vector/vision.py @@ -43,8 +43,7 @@ def __init__(self, robot): self._detect_faces = False self._detect_custom_objects = False - # TODO implement - # self._detect_motion = False + self._detect_motion = False self._display_camera_feed_on_face = False robot.events.subscribe(self._handle_mirror_mode_disabled_event, events.Events.mirror_mode_disabled) @@ -62,7 +61,7 @@ def _handle_mirror_mode_disabled_event(self, _robot, _event_type, _msg): def _handle_vision_modes_auto_disabled_event(self, _robot, _event_type, _msg): self._detect_faces = False self._detect_custom_objects = False - # self._detect_motion = False + self._detect_motion = False self._display_camera_feed_on_face = False @property @@ -73,10 +72,9 @@ def detect_faces(self): def detect_custom_objects(self): return self._detect_custom_objects - # TODO implement - # @property - # def detect_motion(self): - # return self._detect_motion + @property + def detect_motion(self): + return self._detect_motion @property def display_camera_feed_on_face(self): @@ -88,11 +86,12 @@ async def disable_all_vision_modes(self): await self.enable_face_detection(False, False) if self.detect_custom_objects: await self.enable_custom_object_detection(False) - # if self.detect_motion: - # await self.enable_motion_detection(False) + if self.detect_motion: + await self.enable_motion_detection(False) if self.display_camera_feed_on_face: await self.enable_display_camera_feed_on_face(False) + # TODO: add return type hint @connection.on_connection_thread() async def enable_custom_object_detection(self, detect_custom_objects: bool = True): """Enable custom object detection on the robot's camera. @@ -115,6 +114,7 @@ async def enable_custom_object_detection(self, detect_custom_objects: bool = Tru enable_marker_detection_request = protocol.EnableMarkerDetectionRequest(enable=detect_custom_objects) return await self.grpc_interface.EnableMarkerDetection(enable_marker_detection_request) + # TODO: add return type hint @connection.on_connection_thread() async def enable_face_detection( self, @@ -142,24 +142,44 @@ async def enable_face_detection( enable_gaze_detection=False) return await self.grpc_interface.EnableFaceDetection(enable_face_detection_request) - # TODO implement - # @connection.on_connection_thread() - # async def enable_motion_detection(self, detect_motion: bool = True): - # """Enable motion detection on the robot's camera - # - # :param detect_motion: Specify whether we want the robot to detect motion. - # - # .. testcode:: - # - # import anki_vector - # with anki_vector.Robot() as robot: - # robot.vision.enable_motion_detection(detect_motion=True) - # """ - # self._detect_motion = detect_motion - - # enable_motion_detection_request = protocol.EnableMotionDetectionRequest(enable=detect_motion) - # return await self.grpc_interface.EnableMotionDetection(enable_motion_detection_request) + @connection.on_connection_thread() + async def enable_motion_detection(self, detect_motion: bool = True): + """Enable motion detection on the robot's camera + + :param detect_motion: Specify whether we want the robot to detect motion. + + .. testcode:: + + import time + + import anki_vector + from anki_vector.events import Events + from anki_vector.util import degrees + + def on_robot_observed_motion(robot, event_type, event): + print("Robot observed motion") + + with anki_vector.Robot(show_viewer=True) as robot: + robot.events.subscribe(on_robot_observed_motion, Events.robot_observed_motion) + + # If necessary, move Vector's Head and Lift to make it easy to see his face + robot.behavior.set_head_angle(degrees(45.0)) + robot.behavior.set_lift_height(0.0) + + robot.vision.enable_motion_detection(detect_motion=True) + + print("Vector is waiting to see motion. Make some movement within Vector's camera view") + + time.sleep(3.0) + + robot.events.unsubscribe(on_robot_observed_motion, Events.robot_observed_motion) + """ + self._detect_motion = detect_motion + + enable_motion_detection_request = protocol.EnableMotionDetectionRequest(enable=detect_motion) + return await self.grpc_interface.EnableMotionDetection(enable_motion_detection_request) + # TODO: add return type hint @connection.on_connection_thread() async def enable_display_camera_feed_on_face(self, display_camera_feed_on_face: bool = True): """Display the robot's camera feed on its face along with any detections (if enabled) diff --git a/anki_vector/world.py b/anki_vector/world.py index 60485e0..8d1fb58 100644 --- a/anki_vector/world.py +++ b/anki_vector/world.py @@ -788,6 +788,7 @@ def create_custom_fixed_object(self, self._objects[fixed_custom_object.object_id] = fixed_custom_object return fixed_custom_object + # TODO: add return type hint @connection.on_connection_thread(requires_control=False) async def _create_custom_fixed_object(self, pose: util.Pose, From 7efcc14b6bd8266e6cb50cbe3e5fb5187d08e114 Mon Sep 17 00:00:00 2001 From: cyb3rdog Date: Wed, 14 Apr 2021 02:26:28 +0200 Subject: [PATCH 02/15] version 0.7.2.dev2 - added support for EscapePod and EP+OSKR bots - released to pypi as cyb3r_vector_sdk --- README.md | 99 +- anki_vector/configure_pod/__main__.py | 267 +++ anki_vector/connection.py | 1665 ++++++++-------- anki_vector/escapepod.py | 171 ++ anki_vector/robot.py | 1858 +++++++++--------- anki_vector/util.py | 2266 +++++++++++----------- anki_vector/version.py | 30 +- examples/tutorials/00_hello_escapepod.py | 32 + setup.py | 174 +- 9 files changed, 3550 insertions(+), 3012 deletions(-) create mode 100644 anki_vector/configure_pod/__main__.py create mode 100644 anki_vector/escapepod.py create mode 100644 examples/tutorials/00_hello_escapepod.py diff --git a/README.md b/README.md index 5caa72d..91914d7 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,75 @@ -# Anki Vector - Python SDK - -![Vector](docs/source/images/vector-sdk-alpha.jpg) - -Learn more about Vector: https://www.anki.com/en-us/vector - -Learn more about the SDK: https://developer.anki.com/ - -SDK documentation: https://developer.anki.com/vector/docs/index.html - -Forums: https://forums.anki.com/ - - -## Getting Started - -You can follow steps [here](https://developer.anki.com/vector/docs/index.html) to set up your Vector robot with the SDK. - -You can also generate a local copy of the SDK documetation by -following the instructions in the `docs` folder of this project. - - -## Privacy Policy and Terms and Conditions - -Use of Vector and the Vector SDK is subject to Anki's [Privacy Policy](https://www.anki.com/en-us/company/privacy) and [Terms and Conditions](https://www.anki.com/en-us/company/terms-and-conditions). + +# Anki/DDL Vector - Python SDK + +## With support for Production, EscapePod and OSKR robots! + +This is a fork of the original Anki Vector Python SDK. +I have started this fork as an unofficial version to keep things moving. + +![Vector](docs/source/images/vector-sdk-alpha.jpg) + +Learn more about Vector: https://www.anki.com/en-us/vector + +Learn more about how Vector works: [Vector Bible](https://github.com/GooeyChickenman/victor/blob/master/documentation/Vector-TRM.pdf) + +Learn more about the SDK: https://developer.anki.com/ + +SDK documentation: https://developer.anki.com/vector/docs/index.html + +Forums: https://forums.anki.com/ + + +## Getting Started + +You can follow steps [here](https://developer.anki.com/vector/docs/index.html) to set up your Vector robot with the SDK. + +### Installation + +In case you have previously installed the original anki or ikkez sdk, uninstall it with following commands: +``` +pip uninstall anki_vector +pip uninstall ikkez_vector +``` + +To install this SDK fork, run: +``` +pip install cyb3r_vector_sdk +``` + +You can upgrade to latest version with: +``` +pip install cyb3r_vector_sdk --upgrade +``` + +### SDK Configuration + +To condigure the SDK for **Prod**, or **Prod+OSKR** robot, run: + +``` +py -m anki_vector.configure +``` + +To condigure the SDK for **EscapePod**, or **EP+OSKR** robot, run: + +``` +py -m anki_vector.configure_pod +``` + +### SDK Usage - EscapePod + +You can either use the ```anki_vector.configure_pod``` in order to save your authentication into the sdk_config.ini file, and use all the examples and your programs and as you have them, or you can use the Robot object with setting the escape_pod parameter to True, and passing the robot's ip address: + +``` + with anki_vector.Robot(ip="192.168.0.148", escape_pod=True) as robot: + robot.behavior.say_text("Hello Escape Pod") +``` + +### Documentation + +You can generate a local copy of the SDK documetation by +following the instructions in the `docs` folder of this project. + + +## Privacy Policy and Terms and Conditions + +Use of Vector and the Vector SDK is subject to Anki's [Privacy Policy](https://www.anki.com/en-us/company/privacy) and [Terms and Conditions](https://www.anki.com/en-us/company/terms-and-conditions). diff --git a/anki_vector/configure_pod/__main__.py b/anki_vector/configure_pod/__main__.py new file mode 100644 index 0000000..423b8d0 --- /dev/null +++ b/anki_vector/configure_pod/__main__.py @@ -0,0 +1,267 @@ +#!/usr/bin/env python3 + +""" +***Vector EscapePod Python SDK Setup*** + +Vector requires all requests be authorized by an authenticated Anki user. + +This script will enable this device to authenticate with your Vector +robot for use with a Vector Python SDK program. + +Vector must be powered on and connected on the same network as your +computer. By running this script, you will be asked to provide your +Anki account credentials, and the script will download an authentication +token and cert that will grant you access to the robot and his +capabilities (such as camera and audio) as well as data stored on the +robot (such as faces and photos). + +See the README for more information. + +Use of Vector and the Vector SDK is subject to Anki's Privacy Policy and Terms and Conditions. + +https://www.anki.com/en-us/company/privacy +https://www.anki.com/en-us/company/terms-and-conditions + +""" + +import argparse +import configparser +from getpass import getpass +import json +import os +from pathlib import Path +import platform +import re +import socket +import sys +import ssl + +from cryptography import x509 +from cryptography.hazmat.backends import default_backend +import grpc +import requests +try: + from termcolor import colored # pylint: disable=import-error +except: # pylint: disable=bare-except + def colored(text, color=None, on_color=None, attrs=None): # pylint: disable=unused-argument + return text + +import anki_vector +from anki_vector import messaging + + +def get_serial(serial=None): + if not serial: + serial = os.environ.get('ANKI_ROBOT_SERIAL') + if not serial: + print("\n\nPlease find your robot serial number (ex. 00e20100) located on the underside of Vector, or accessible from Vector's debug screen.") + serial = input('Enter robot serial number: ') + else: + print("Found robot serial number in environment variable '{}'".format(colored("ANKI_ROBOT_SERIAL", "green"))) + serial = serial.lower() + print("Using robot serial number: {}".format(colored(serial, "cyan"))) + return serial + + +def get_cert(hostname=None): + print("\nDownloading Vector certificate...", end="") + sys.stdout.flush() + + conn = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + context = ssl.SSLContext(ssl.PROTOCOL_SSLv23) + sock = context.wrap_socket(conn, server_hostname=hostname) + sock.connect((hostname, 443)) + cert = ssl.DER_cert_to_PEM_cert(sock.getpeercert(True)) + return str.encode(cert) + + +def user_authentication(session_id: bytes, cert: bytes, ip: str, name: str) -> str: + # Pin the robot certificate for opening the channel + creds = grpc.ssl_channel_credentials(root_certificates=cert) + + print("Attempting to download guid from {} at {}:443...".format(colored(name, "cyan"), colored(ip, "cyan")), end="") + sys.stdout.flush() + channel = grpc.secure_channel("{}:443".format(ip), creds, + options=(("grpc.ssl_target_name_override", name,),)) + + # Verify the connection to Vector is able to be established (client-side) + try: + # Explicitly grab _channel._channel to test the underlying grpc channel directly + grpc.channel_ready_future(channel).result(timeout=15) + except grpc.FutureTimeoutError: + print(colored(" ERROR", "red")) + sys.exit("\nUnable to connect to Vector\n" + "Please be sure to connect via the Vector companion app first, and connect your computer to the same network as your Vector.") + + try: + interface = messaging.client.ExternalInterfaceStub(channel) + request = messaging.protocol.UserAuthenticationRequest( + user_session_id=session_id.encode('utf-8'), + client_name=socket.gethostname().encode('utf-8')) + response = interface.UserAuthentication(request) + if response.code != messaging.protocol.UserAuthenticationResponse.AUTHORIZED: # pylint: disable=no-member + print(colored(" ERROR", "red")) + sys.exit("\nFailed to authorize request:\n" + "Please be sure to first set up Vector using the companion app.") + except grpc.RpcError as e: + print(colored(" ERROR", "red")) + sys.exit("\nFailed to authorize request:\n" + "An unknown error occurred '{}'".format(e)) + + print(colored(" DONE\n", "green")) + return response.client_token_guid + + + +def standardize_name(robot_name): + # Extend the name if not enough is provided + if len(robot_name) == 4: + robot_name = "Vector-{}".format(robot_name.upper()) + # Fix possible capitalization and space/dash/etc. + if re.match("[Vv]ector.[A-Za-z0-9]{4}", robot_name): + robot_name = "V{}-{}".format(robot_name[1:-5], robot_name[-4:].upper()) + # Check that the end is valid + if re.match("Vector-[A-Z0-9]{4}", robot_name): + return robot_name + print(colored(" ERROR", "red")) + sys.exit("Invalid robot name. Please match the format exactly. Example: Vector-A1B2") + + +def get_name_and_ip(robot_name=None, ip=None): + if not robot_name: + robot_name = os.environ.get('VECTOR_ROBOT_NAME') + if not robot_name: + print("\n\nFind your robot name (ex. Vector-A1B2) by placing Vector on the charger and double-clicking Vector's backpack button.") + robot_name = input("Enter robot name: ") + else: + print("Found robot name in environment variable '{}'".format(colored("VECTOR_ROBOT_NAME", "green"))) + robot_name = standardize_name(robot_name) + print("Using robot name: {}".format(colored(robot_name, "cyan"))) + if not ip: + ip = os.environ.get('ANKI_ROBOT_HOST') + if not ip: + print("\n\nFind your robot ip address (ex. 192.168.42.42) by placing Vector on the charger, double-clicking Vector's backpack button,\n" + "then raising and lowering his arms. If you see {} on his face, reconnect Vector to your WiFi using the Vector Companion App.".format(colored("XX.XX.XX.XX", "red"))) + ip = input("Enter robot ip: ") + else: + print("Found robot ip address in environment variable '{}'".format(colored("ANKI_ROBOT_HOST", "green"))) + print("Using IP: {}".format(colored(ip, "cyan"))) + return robot_name, ip + + +def save_cert(cert, name, serial, anki_dir): + """Write Vector's certificate to a file located in the user's home directory""" + os.makedirs(str(anki_dir), exist_ok=True) + cert_file = str(anki_dir / "{name}-{serial}.cert".format(name=name, serial=serial)) + print("Writing certificate file to '{}'...\n".format(colored(cert_file, "cyan"))) + with os.fdopen(os.open(cert_file, os.O_WRONLY | os.O_CREAT, 0o600), 'wb') as f: + f.write(cert) + return cert_file + + +def validate_cert_name(cert_file, robot_name): + """Validate the name on Vector's certificate against the user-provided name""" + with open(cert_file, "rb") as f: + cert_file = f.read() + cert = x509.load_pem_x509_certificate(cert_file, default_backend()) + for fields in cert.subject: + current = str(fields.oid) + if "commonName" in current: + common_name = fields.value + if common_name != robot_name: + print(colored(" ERROR", "red")) + sys.exit("The name of the certificate ({}) does not match the name provided ({}).\n" + "Please verify the name, and try again.".format(common_name, robot_name)) + else: + return + + +def write_config(serial, cert_file=None, ip=None, name=None, guid=None, clear=True): + home = Path.home() + config_file = str(home / ".anki_vector" / "sdk_config.ini") + print("Writing config file to '{}'...".format(colored(config_file, "cyan"))) + + config = configparser.ConfigParser(strict=False) + + try: + config.read(config_file) + except configparser.ParsingError: + if os.path.exists(config_file): + os.rename(config_file, config_file + "-error") + if clear: + config[serial] = {} + if cert_file: + config[serial]["cert"] = cert_file + if ip: + config[serial]["ip"] = ip + if name: + config[serial]["name"] = name + if guid: + config[serial]["guid"] = guid.decode("utf-8") + temp_file = config_file + "-temp" + if os.path.exists(config_file): + os.rename(config_file, temp_file) + try: + with os.fdopen(os.open(config_file, os.O_WRONLY | os.O_CREAT, 0o600), 'w') as f: + config.write(f) + except Exception as e: + if os.path.exists(temp_file): + os.rename(temp_file, config_file) + raise e + else: + if os.path.exists(temp_file): + os.remove(temp_file) + + +def main(): + parser = argparse.ArgumentParser(description=("Vector requires all requests be authorized by an authenticated Anki user. " + "This script will enable this device to authenticate with your Vector " + "robot for use with a Vector Python SDK program."), + epilog=("See the README for more information. " + "Use of Vector and the Vector SDK is subject to Anki's Privacy Policy and Terms and Conditions. " + "https://www.anki.com/en-us/company/privacy and " + "https://www.anki.com/en-us/company/terms-and-conditions")) + parser.add_argument("-i", "--ip", help=("Your robot ip address (ex. 192.168.42.42). " + "It may be found by placing Vector on the charger, " + "double-clicking Vector's backpack button, " + "then raising and lowering his arms. " + "If you see {} on his face, " + "reconnect Vector to your WiFi using the Vector Companion App.".format(colored("XX.XX.XX.XX", "red")))) + parser.add_argument("-n", "--name", help=("Your robot name (ex. Vector-A1B2). " + "It may be found by placing Vector on the charger and double-clicking Vector's backpack button.")) + parser.add_argument("-s", "--serial", help=("Your robot serial number (ex. 00e20100). " + "It is located on the underside of Vector, or accessible from Vector's debug screen.")) + parser.add_argument("-u", "--update", dest="new_ip", help=("Update the stored ip for Vector. This makes it easier to transfer between networks.")) + args = parser.parse_args() + + if args.new_ip: + serial = get_serial(args.serial) + write_config(serial, ip=args.new_ip, clear=False) + print(colored("\nIP Updated!", "green")) + sys.exit() + + print(__doc__) + + valid = ["y", "Y", "yes", "YES"] + environ = input("Do you wish to proceed? (y/n) ") + if environ not in valid: + sys.exit("Stopping...") + + name, ip = get_name_and_ip(args.name, args.ip) + serial = get_serial(args.serial) + cert = get_cert(ip) + + home = Path.home() + anki_dir = home / ".anki_vector" + + cert_file = save_cert(cert, name, serial, anki_dir) + validate_cert_name(cert_file, name) + + guid = user_authentication("Anything1", cert, ip, name) + + # Store credentials in the .anki_vector directory's sdk_config.ini file + write_config(serial, cert_file, ip, name, guid) + print(colored("\nSUCCESS!", "green")) + +if __name__ == "__main__": + main() diff --git a/anki_vector/connection.py b/anki_vector/connection.py index 86c0581..9c04bc1 100644 --- a/anki_vector/connection.py +++ b/anki_vector/connection.py @@ -1,827 +1,838 @@ -# Copyright (c) 2018 Anki, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License in the file LICENSE.txt or at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -Management of the connection to and from Vector. -""" - -# __all__ should order by constants, event classes, other classes, functions. -__all__ = ['ControlPriorityLevel', 'Connection', 'on_connection_thread'] - -import asyncio -from concurrent import futures -from enum import Enum -import functools -import inspect -import logging -import platform -import sys -import threading -from typing import Any, Awaitable, Callable, Coroutine, Dict, List - -from google.protobuf.text_format import MessageToString -import grpc -import aiogrpc - -from . import util -from .exceptions import (connection_error, - VectorAsyncException, - VectorBehaviorControlException, - VectorConfigurationException, - VectorControlException, - VectorControlTimeoutException, - VectorInvalidVersionException, - VectorNotFoundException) -from .messaging import client, protocol -from .version import __version__ - - -class CancelType(Enum): - """Enum used to specify cancellation options for behaviors -- internal use only """ - #: Cancellable as an 'Action' - CANCELLABLE_ACTION = 0 - #: Cancellable as a 'Behavior' - CANCELLABLE_BEHAVIOR = 1 - - -class ControlPriorityLevel(Enum): - """Enum used to specify the priority level for the program.""" - #: Runs above mandatory physical reactions, will drive off table, perform while on a slope, - #: ignore low battery state, work in the dark, etc. - OVERRIDE_BEHAVIORS_PRIORITY = protocol.ControlRequest.OVERRIDE_BEHAVIORS # pylint: disable=no-member - #: Runs below Mandatory Physical Reactions such as tucking Vector's head and arms during a fall, - #: yet above Trigger-Word Detection. Default for normal operation. - DEFAULT_PRIORITY = protocol.ControlRequest.DEFAULT # pylint: disable=no-member - #: Holds control of robot before/after other SDK connections - #: Used to disable idle behaviors. Not to be used for regular behavior control. - RESERVE_CONTROL = protocol.ControlRequest.RESERVE_CONTROL # pylint: disable=no-member - - -class _ControlEventManager: - """This manages every :class:`asyncio.Event` that handles the behavior control - system. - - These include three events: granted, lost, and request. - - :class:`granted_event` represents the behavior system handing control to the SDK. - - :class:`lost_event` represents a higher priority behavior taking control away from the SDK. - - :class:`request_event` Is a way of alerting :class:`Connection` to request control. - """ - - def __init__(self, loop: asyncio.BaseEventLoop = None, priority: ControlPriorityLevel = None): - self._granted_event = asyncio.Event(loop=loop) - self._lost_event = asyncio.Event(loop=loop) - self._request_event = asyncio.Event(loop=loop) - self._has_control = False - self._priority = priority - self._is_shutdown = False - - @property - def granted_event(self) -> asyncio.Event: - """This event is used to notify listeners that control has been granted to the SDK.""" - return self._granted_event - - @property - def lost_event(self) -> asyncio.Event: - """Represents a higher priority behavior taking control away from the SDK.""" - return self._lost_event - - @property - def request_event(self) -> asyncio.Event: - """Used to alert :class:`Connection` to request control.""" - return self._request_event - - @property - def has_control(self) -> bool: - """Check to see that the behavior system has control (without blocking by checking :class:`granted_event`)""" - return self._has_control - - @property - def priority(self) -> ControlPriorityLevel: - """The currently desired priority for the SDK.""" - return self._priority - - @property - def is_shutdown(self) -> bool: - """Detect if the behavior control stream is supposed to shut down.""" - return self._is_shutdown - - def request(self, priority: ControlPriorityLevel = ControlPriorityLevel.DEFAULT_PRIORITY) -> None: - """Tell the behavior stream to request control via setting the :class:`request_event`. - - This will signal Connection's :func:`_request_handler` generator to send a request control message on the BehaviorControl stream. - This signal happens asynchronously, and can be tracked using the :class:`granted_event` parameter. - - :param priority: The level of control in the behavior system. This determines which actions are allowed to - interrupt the SDK execution. See :class:`ControlPriorityLevel` for more information. - """ - if priority is None: - raise VectorBehaviorControlException("Must provide a priority level to request. To disable control, use {}.release().", self.__class__.__name__) - self._priority = priority - self._request_event.set() - - def release(self) -> None: - """Tell the behavior stream to release control via setting the :class:`request_event` while priority is ``None``. - - This will signal Connection's :func:`_request_handler` generator to send a release control message on the BehaviorControl stream. - This signal happens asynchronously, and can be tracked using the :class:`lost_event` parameter. - """ - self._priority = None - self._request_event.set() - - def update(self, enabled: bool) -> None: - """Update the current state of control (either enabled or disabled) - - :param enabled: Used to enable/disable behavior control - """ - self._has_control = enabled - if enabled: - self._granted_event.set() - self._lost_event.clear() - else: - self._lost_event.set() - self._granted_event.clear() - - def shutdown(self) -> None: - """Tells the control stream to shut down. - - This will return control to the rest of the behavior system. - """ - self._has_control = False - self._granted_event.set() - self._lost_event.set() - self._is_shutdown = True - self._request_event.set() - - -class Connection: - """Creates and maintains a aiogrpc connection including managing the connection thread. - The connection thread decouples the actual messaging layer from the user's main thread, - and requires any network requests to be ran using :func:`asyncio.run_coroutine_threadsafe` - to make them run on the other thread. Connection provides two helper functions for running - a function on the connection thread: :func:`~Connection.run_coroutine` and - :func:`~Connection.run_soon`. - - This class may be used to bypass the structures of the python sdk handled by - :class:`~anki_vector.robot.Robot`, and instead talk to aiogrpc more directly. - - The values for the cert_file location and the guid can be found in your home directory in - the sdk_config.ini file. - - .. code-block:: python - - import anki_vector - - # Connect to your Vector - conn = anki_vector.connection.Connection("Vector-XXXX", "XX.XX.XX.XX:443", "/path/to/file.cert", "") - conn.connect() - # Run your commands - async def play_animation(): - # Run your commands - anim = anki_vector.messaging.protocol.Animation(name="anim_pounce_success_02") - anim_request = anki_vector.messaging.protocol.PlayAnimationRequest(animation=anim) - return await conn.grpc_interface.PlayAnimation(anim_request) # This needs to be run in an asyncio loop - conn.run_coroutine(play_animation()).result() - # Close the connection - conn.close() - - :param name: Vector's name in the format of "Vector-XXXX". - :param host: The IP address and port of Vector in the format "XX.XX.XX.XX:443". - :param cert_file: The location of the certificate file on disk. - :param guid: Your robot's unique secret key. - :param behavior_control_level: pass one of :class:`ControlPriorityLevel` priority levels if the connection - requires behavior control, or None to decline control. - """ - - def __init__(self, name: str, host: str, cert_file: str, guid: str, behavior_control_level: ControlPriorityLevel = ControlPriorityLevel.DEFAULT_PRIORITY): - if cert_file is None: - raise VectorConfigurationException("Must provide a cert file to authenticate to Vector.") - self._loop: asyncio.BaseEventLoop = None - self.name = name - self.host = host - self.cert_file = cert_file - self._interface = None - self._channel = None - self._has_control = False - self._logger = util.get_class_logger(__name__, self) - self._control_stream_task = None - self._control_events: _ControlEventManager = None - self._guid = guid - self._thread: threading.Thread = None - self._ready_signal: threading.Event = threading.Event() - self._done_signal: asyncio.Event = None - self._conn_exception = False - self._behavior_control_level = behavior_control_level - self.active_commands = [] - - @property - def loop(self) -> asyncio.BaseEventLoop: - """A direct reference to the loop on the connection thread. - Can be used to run functions in on thread. - - .. testcode:: - - import anki_vector - import asyncio - - async def connection_function(): - print("I'm running in the connection thread event loop.") - - with anki_vector.Robot() as robot: - asyncio.run_coroutine_threadsafe(connection_function(), robot.conn.loop) - - :returns: The loop running inside the connection thread - """ - if self._loop is None: - raise VectorAsyncException("Attempted to access the connection loop before it was ready") - return self._loop - - @property - def thread(self) -> threading.Thread: - """A direct reference to the connection thread. Available to callers to determine if the - current thread is the connection thread. - - .. testcode:: - - import anki_vector - import threading - - with anki_vector.Robot() as robot: - if threading.current_thread() is robot.conn.thread: - print("This code is running on the connection thread") - else: - print("This code is not running on the connection thread") - - :returns: The connection thread where all of the grpc messages are being processed. - """ - if self._thread is None: - raise VectorAsyncException("Attempted to access the connection loop before it was ready") - return self._thread - - @property - def grpc_interface(self) -> client.ExternalInterfaceStub: - """A direct reference to the connected aiogrpc interface. - - This may be used to directly call grpc messages bypassing :class:`anki_vector.Robot` - - .. code-block:: python - - import anki_vector - - # Connect to your Vector - conn = anki_vector.connection.Connection("Vector-XXXX", "XX.XX.XX.XX:443", "/path/to/file.cert", "") - conn.connect() - # Run your commands - async def play_animation(): - # Run your commands - anim = anki_vector.messaging.protocol.Animation(name="anim_pounce_success_02") - anim_request = anki_vector.messaging.protocol.PlayAnimationRequest(animation=anim) - return await conn.grpc_interface.PlayAnimation(anim_request) # This needs to be run in an asyncio loop - conn.run_coroutine(play_animation()).result() - # Close the connection - conn.close() - """ - return self._interface - - @property - def behavior_control_level(self) -> ControlPriorityLevel: - """Returns the specific :class:`ControlPriorityLevel` requested for behavior control. - - To be able to directly control Vector's motors, override his screen, play an animation, etc., - the :class:`Connection` will need behavior control. This property identifies the enumerated - level of behavior control that the SDK will maintain over the robot. - - For more information about behavior control, see :ref:`behavior `. - - .. code-block:: python - - import anki_vector - - with anki_vector.Robot() as robot: - print(robot.conn.behavior_control_level) # Will print ControlPriorityLevel.DEFAULT_PRIORITY - robot.conn.release_control() - print(robot.conn.behavior_control_level) # Will print None - """ - return self._behavior_control_level - - @property - def requires_behavior_control(self) -> bool: - """True if the :class:`Connection` requires behavior control. - - To be able to directly control Vector's motors, override his screen, play an animation, etc., - the :class:`Connection` will need behavior control. This boolean signifies that - the :class:`Connection` will try to maintain control of Vector's behavior system even after losing - control to higher priority robot behaviors such as returning home to charge a low battery. - - For more information about behavior control, see :ref:`behavior `. - - .. code-block:: python - - import time - - import anki_vector - - def callback(robot, event_type, event): - robot.conn.request_control() - print(robot.conn.requires_behavior_control) # Will print True - robot.anim.play_animation_trigger('GreetAfterLongTime') - robot.conn.release_control() - - with anki_vector.Robot(behavior_control_level=None) as robot: - print(robot.conn.requires_behavior_control) # Will print False - robot.events.subscribe(callback, anki_vector.events.Events.robot_observed_face) - - # Waits 10 seconds. Show Vector your face. - time.sleep(10) - """ - return self._behavior_control_level is not None - - @property - def control_lost_event(self) -> asyncio.Event: - """This provides an :class:`asyncio.Event` that a user may :func:`wait()` upon to - detect when Vector has taken control of the behavior system at a higher priority. - - .. testcode:: - - import anki_vector - - async def auto_reconnect(conn: anki_vector.connection.Connection): - await conn.control_lost_event.wait() - conn.request_control() - """ - return self._control_events.lost_event - - @property - def control_granted_event(self) -> asyncio.Event: - """This provides an :class:`asyncio.Event` that a user may :func:`wait()` upon to - detect when Vector has given control of the behavior system to the SDK program. - - .. testcode:: - - import anki_vector - - async def wait_for_control(conn: anki_vector.connection.Connection): - await conn.control_granted_event.wait() - # Run commands that require behavior control - """ - return self._control_events.granted_event - - def request_control(self, behavior_control_level: ControlPriorityLevel = ControlPriorityLevel.DEFAULT_PRIORITY, timeout: float = 10.0): - """Explicitly request behavior control. Typically used after detecting :func:`control_lost_event`. - - To be able to directly control Vector's motors, override his screen, play an animation, etc., - the :class:`Connection` will need behavior control. This function will acquire control - of Vector's behavior system. This will raise a :class:`VectorControlTimeoutException` if it fails - to gain control before the timeout. - - For more information about behavior control, see :ref:`behavior ` - - .. testcode:: - - import anki_vector - - async def auto_reconnect(conn: anki_vector.connection.Connection): - await conn.control_lost_event.wait() - conn.request_control(timeout=5.0) - - :param timeout: The time allotted to attempt a connection, in seconds. - :param behavior_control_level: request control of Vector's behavior system at a specific level of control. - See :class:`ControlPriorityLevel` for more information. - """ - if not isinstance(behavior_control_level, ControlPriorityLevel): - raise TypeError("behavior_control_level must be of type ControlPriorityLevel") - if self._thread is threading.current_thread(): - return asyncio.ensure_future(self._request_control(behavior_control_level=behavior_control_level, timeout=timeout), loop=self._loop) - return self.run_coroutine(self._request_control(behavior_control_level=behavior_control_level, timeout=timeout)) - - async def _request_control(self, behavior_control_level: ControlPriorityLevel = ControlPriorityLevel.DEFAULT_PRIORITY, timeout: float = 10.0): - self._behavior_control_level = behavior_control_level - self._control_events.request(self._behavior_control_level) - try: - self._has_control = await asyncio.wait_for(self.control_granted_event.wait(), timeout) - except futures.TimeoutError as e: - raise VectorControlTimeoutException(f"Surpassed timeout of {timeout}s") from e - - def release_control(self, timeout: float = 10.0): - """Explicitly release control. Typically used after detecting :func:`control_lost_event`. - - To be able to directly control Vector's motors, override his screen, play an animation, etc., - the :class:`Connection` will need behavior control. This function will release control - of Vector's behavior system. This will raise a :class:`VectorControlTimeoutException` if it fails - to receive a control_lost event before the timeout. - - .. testcode:: - - import anki_vector - - async def wait_for_control(conn: anki_vector.connection.Connection): - await conn.control_granted_event.wait() - # Run commands that require behavior control - conn.release_control() - - :param timeout: The time allotted to attempt to release control, in seconds. - """ - if self._thread is threading.current_thread(): - return asyncio.ensure_future(self._release_control(timeout=timeout), loop=self._loop) - return self.run_coroutine(self._release_control(timeout=timeout)) - - async def _release_control(self, timeout: float = 10.0): - self._behavior_control_level = None - self._control_events.release() - try: - self._has_control = await asyncio.wait_for(self.control_lost_event.wait(), timeout) - except futures.TimeoutError as e: - raise VectorControlTimeoutException(f"Surpassed timeout of {timeout}s") from e - - def connect(self, timeout: float = 10.0) -> None: - """Connect to Vector. This will start the connection thread which handles all messages - between Vector and Python. - - .. code-block:: python - - import anki_vector - - # Connect to your Vector - conn = anki_vector.connection.Connection("Vector-XXXX", "XX.XX.XX.XX:443", "/path/to/file.cert", "") - conn.connect() - # Run your commands - async def play_animation(): - # Run your commands - anim = anki_vector.messaging.protocol.Animation(name="anim_pounce_success_02") - anim_request = anki_vector.messaging.protocol.PlayAnimationRequest(animation=anim) - return await conn.grpc_interface.PlayAnimation(anim_request) # This needs to be run in an asyncio loop - conn.run_coroutine(play_animation()).result() - # Close the connection - conn.close() - - :param timeout: The time allotted to attempt a connection, in seconds. - """ - if self._thread: - raise VectorAsyncException("\n\nRepeated connections made to open Connection.") - self._ready_signal.clear() - self._thread = threading.Thread(target=self._connect, args=(timeout,), daemon=True, name="gRPC Connection Handler Thread") - self._thread.start() - ready = self._ready_signal.wait(timeout=2 * timeout) - if not ready: - raise VectorNotFoundException() - if hasattr(self._ready_signal, "exception"): - e = getattr(self._ready_signal, "exception") - delattr(self._ready_signal, "exception") - raise e - - def _connect(self, timeout: float) -> None: - """The function that runs on the connection thread. This will connect to Vector, - and establish the BehaviorControl stream. - """ - try: - if threading.main_thread() is threading.current_thread(): - raise VectorAsyncException("\n\nConnection._connect must be run outside of the main thread.") - self._loop = asyncio.new_event_loop() - asyncio.set_event_loop(self._loop) - self._done_signal = asyncio.Event() - if not self._behavior_control_level: - self._control_events = _ControlEventManager(self._loop) - else: - self._control_events = _ControlEventManager(self._loop, priority=self._behavior_control_level) - trusted_certs = None - with open(self.cert_file, 'rb') as cert: - trusted_certs = cert.read() - - # Pin the robot certificate for opening the channel - channel_credentials = aiogrpc.ssl_channel_credentials(root_certificates=trusted_certs) - # Add authorization header for all the calls - call_credentials = aiogrpc.access_token_call_credentials(self._guid) - - credentials = aiogrpc.composite_channel_credentials(channel_credentials, call_credentials) - - self._logger.info(f"Connecting to {self.host} for {self.name} using {self.cert_file}") - self._channel = aiogrpc.secure_channel(self.host, credentials, - options=(("grpc.ssl_target_name_override", self.name,),)) - - # Verify the connection to Vector is able to be established (client-side) - try: - # Explicitly grab _channel._channel to test the underlying grpc channel directly - grpc.channel_ready_future(self._channel._channel).result(timeout=timeout) # pylint: disable=protected-access - except grpc.FutureTimeoutError as e: - raise VectorNotFoundException() from e - - self._interface = client.ExternalInterfaceStub(self._channel) - - # Verify Vector and the SDK have compatible protocol versions - version = protocol.ProtocolVersionRequest(client_version=protocol.PROTOCOL_VERSION_CURRENT, min_host_version=protocol.PROTOCOL_VERSION_MINIMUM) - protocol_version = self._loop.run_until_complete(self._interface.ProtocolVersion(version)) - if protocol_version.result != protocol.ProtocolVersionResponse.SUCCESS or protocol.PROTOCOL_VERSION_MINIMUM > protocol_version.host_version: # pylint: disable=no-member - raise VectorInvalidVersionException(protocol_version) - - self._control_stream_task = self._loop.create_task(self._open_connections()) - - # Initialze SDK - sdk_module_version = __version__ - python_version = platform.python_version() - python_implementation = platform.python_implementation() - os_version = platform.platform() - cpu_version = platform.machine() - initialize = protocol.SDKInitializationRequest(sdk_module_version=sdk_module_version, - python_version=python_version, - python_implementation=python_implementation, - os_version=os_version, - cpu_version=cpu_version) - self._loop.run_until_complete(self._interface.SDKInitialization(initialize)) - - if self._behavior_control_level: - self._loop.run_until_complete(self._request_control(behavior_control_level=self._behavior_control_level, timeout=timeout)) - except Exception as e: # pylint: disable=broad-except - # Propagate the errors to the calling thread - setattr(self._ready_signal, "exception", e) - self._loop.close() - return - finally: - self._ready_signal.set() - - try: - async def wait_until_done(): - return await self._done_signal.wait() - self._loop.run_until_complete(wait_until_done()) - finally: - self._loop.close() - - async def _request_handler(self): - """Handles generating messages for the BehaviorControl stream.""" - while await self._control_events.request_event.wait(): - self._control_events.request_event.clear() - if self._control_events.is_shutdown: - return - priority = self._control_events.priority - if priority is None: - msg = protocol.ControlRelease() - msg = protocol.BehaviorControlRequest(control_release=msg) - else: - msg = protocol.ControlRequest(priority=priority.value) - msg = protocol.BehaviorControlRequest(control_request=msg) - self._logger.debug(f"BehaviorControl {MessageToString(msg, as_one_line=True)}") - yield msg - - async def _open_connections(self): - """Starts the BehaviorControl stream, and handles the messages coming back from the robot.""" - try: - async for response in self._interface.BehaviorControl(self._request_handler()): - response_type = response.WhichOneof("response_type") - if response_type == 'control_granted_response': - self._logger.info(f"BehaviorControl {MessageToString(response, as_one_line=True)}") - self._control_events.update(True) - elif response_type == 'control_lost_event': - self._cancel_active() - self._logger.info(f"BehaviorControl {MessageToString(response, as_one_line=True)}") - self._control_events.update(False) - except futures.CancelledError: - self._logger.debug('Behavior handler task was cancelled. This is expected during disconnection.') - - def _cancel_active(self): - for fut in self.active_commands: - if not fut.done(): - fut.cancel() - self.active_commands = [] - - def close(self): - """Cleanup the connection, and shutdown all the event handlers. - - Usually this should be invoked by the Robot class when it closes. - - .. code-block:: python - - import anki_vector - - # Connect to your Vector - conn = anki_vector.connection.Connection("Vector-XXXX", "XX.XX.XX.XX:443", "/path/to/file.cert", "") - conn.connect() - # Run your commands - async def play_animation(): - # Run your commands - anim = anki_vector.messaging.protocol.Animation(name="anim_pounce_success_02") - anim_request = anki_vector.messaging.protocol.PlayAnimationRequest(animation=anim) - return await conn.grpc_interface.PlayAnimation(anim_request) # This needs to be run in an asyncio loop - conn.run_coroutine(play_animation()).result() - # Close the connection - conn.close() - """ - if self._control_events: - self._control_events.shutdown() - if self._control_stream_task: - self._control_stream_task.cancel() - self.run_coroutine(self._control_stream_task).result() - self._cancel_active() - if self._channel: - self.run_coroutine(self._channel.close()).result() - self.run_coroutine(self._done_signal.set) - self._thread.join(timeout=5) - self._thread = None - - def run_soon(self, coro: Awaitable) -> None: - """Schedules the given awaitable to run on the event loop for the connection thread. - - .. testcode:: - - import anki_vector - import time - - async def my_coroutine(): - print("Running on the connection thread") - - with anki_vector.Robot() as robot: - robot.conn.run_soon(my_coroutine()) - time.sleep(1) - - :param coro: The coroutine, task or any awaitable to schedule for execution on the connection thread. - """ - if coro is None or not inspect.isawaitable(coro): - raise VectorAsyncException(f"\n\n{coro.__name__ if hasattr(coro, '__name__') else coro} is not awaitable, so cannot be ran with run_soon.\n") - - def soon(): - try: - asyncio.ensure_future(coro) - except TypeError as e: - raise VectorAsyncException(f"\n\n{coro.__name__ if hasattr(coro, '__name__') else coro} could not be ensured as a future.\n") from e - if threading.current_thread() is self._thread: - self._loop.call_soon(soon) - else: - self._loop.call_soon_threadsafe(soon) - - def run_coroutine(self, coro: Awaitable) -> Any: - """Runs a given awaitable on the connection thread's event loop. - Cannot be called from within the connection thread. - - .. testcode:: - - import anki_vector - - async def my_coroutine(): - print("Running on the connection thread") - return "Finished" - - with anki_vector.Robot() as robot: - result = robot.conn.run_coroutine(my_coroutine()) - - :param coro: The coroutine, task or any other awaitable which should be executed. - :returns: The result of the awaitable's execution. - """ - if threading.current_thread() is self._thread: - raise VectorAsyncException("Attempting to invoke async from same thread." - "Instead you may want to use 'run_soon'") - if asyncio.iscoroutinefunction(coro) or asyncio.iscoroutine(coro): - return self._run_coroutine(coro) - if asyncio.isfuture(coro): - async def future_coro(): - return await coro - return self._run_coroutine(future_coro()) - if callable(coro): - async def wrapped_coro(): - return coro() - return self._run_coroutine(wrapped_coro()) - raise VectorAsyncException("\n\nInvalid parameter to run_coroutine: {}\n" - "This function expects a coroutine, task, or awaitable.".format(type(coro))) - - def _run_coroutine(self, coro): - return asyncio.run_coroutine_threadsafe(coro, self._loop) - - -def on_connection_thread(log_messaging: bool = True, requires_control: bool = True, is_cancellable: CancelType = None) -> Callable[[Coroutine[util.Component, Any, None]], Any]: - """A decorator generator used internally to denote which functions will run on - the connection thread. This unblocks the caller of the wrapped function - and allows them to continue running while the messages are being processed. - - .. code-block:: python - - import anki_vector - - class MyComponent(anki_vector.util.Component): - @connection._on_connection_thread() - async def on_connection_thread(self): - # Do work on the connection thread - - :param log_messaging: True if the log output should include the entire message or just the size. Recommended for - large binary return values. - :param requires_control: True if the function should wait until behavior control is granted before executing. - :param is_cancellable: use a valid enum of :class:`CancelType` to specify the type of cancellation for the - function. Defaults to 'None' implying no support for responding to cancellation. - :returns: A decorator which has 3 possible returns based on context: the result of the decorated function, - the :class:`concurrent.futures.Future` which points to the decorated function, or the - :class:`asyncio.Future` which points to the decorated function. - These contexts are: when the robot is a :class:`~anki_vector.robot.Robot`, - when the robot is an :class:`~anki_vector.robot.AsyncRobot`, and when - called from the connection thread respectively. - """ - def _on_connection_thread_decorator(func: Coroutine) -> Any: - """A decorator which specifies a function to be executed on the connection thread - - :params func: The function to be decorated - :returns: There are 3 possible returns based on context: the result of the decorated function, - the :class:`concurrent.futures.Future` which points to the decorated function, or the - :class:`asyncio.Future` which points to the decorated function. - These contexts are: when the robot is a :class:`anki_vector.robot.Robot`, - when the robot is an :class:`anki_vector.robot.AsyncRobot`, and when - called from the connection thread respectively. - """ - if not asyncio.iscoroutinefunction(func): - raise VectorAsyncException("\n\nCannot define non-coroutine function '{}' to run on connection thread.\n" - "Make sure the function is defined using 'async def'.".format(func.__name__ if hasattr(func, "__name__") else func)) - - @functools.wraps(func) - async def log_handler(conn: Connection, func: Coroutine, logger: logging.Logger, *args: List[Any], **kwargs: Dict[str, Any]) -> Coroutine: - """Wrap the provided coroutine to better express exceptions as specific :class:`anki_vector.exceptions.VectorException`s, and - adds logging to incoming (from the robot) and outgoing (to the robot) messages. - """ - result = None - # TODO: only have the request wait for control if we're not done. If done raise an exception. - control = conn.control_granted_event - if requires_control and not control.is_set(): - if not conn.requires_behavior_control: - raise VectorControlException(func.__name__) - logger.info(f"Delaying {func.__name__} until behavior control is granted") - await asyncio.wait([conn.control_granted_event.wait()], timeout=10) - message = args[1:] - outgoing = message if log_messaging else "size = {} bytes".format(sys.getsizeof(message)) - logger.debug(f'Outgoing {func.__name__}: {outgoing}') - try: - result = await func(*args, **kwargs) - except grpc.RpcError as rpc_error: - raise connection_error(rpc_error) from rpc_error - incoming = str(result).strip() if log_messaging else "size = {} bytes".format(sys.getsizeof(result)) - logger.debug(f'Incoming {func.__name__}: {type(result).__name__} {incoming}') - return result - - @functools.wraps(func) - def result(*args: List[Any], **kwargs: Dict[str, Any]) -> Any: - """The function that is the result of the decorator. Provides a wrapped function. - - :param _return_future: A hidden parameter which allows the wrapped function to explicitly - return a future (default for AsyncRobot) or not (default for Robot). - :returns: Based on context this can return the result of the decorated function, - the :class:`concurrent.futures.Future` which points to the decorated function, or the - :class:`asyncio.Future` which points to the decorated function. - These contexts are: when the robot is a :class:`anki_vector.robot.Robot`, - when the robot is an :class:`anki_vector.robot.AsyncRobot`, and when - called from the connection thread respectively.""" - self = args[0] # Get the self reference from the function call - # if the call supplies a _return_future parameter then override force_async with that. - _return_future = kwargs.pop('_return_future', self.force_async) - - action_id = None - if is_cancellable == CancelType.CANCELLABLE_ACTION: - action_id = self._get_next_action_id() - kwargs['_action_id'] = action_id - - wrapped_coroutine = log_handler(self.conn, func, self.logger, *args, **kwargs) - - if threading.current_thread() == self.conn.thread: - if self.conn.loop.is_running(): - return asyncio.ensure_future(wrapped_coroutine, loop=self.conn.loop) - raise VectorAsyncException("\n\nThe connection thread loop is not running, but a " - "function '{}' is being invoked on that thread.\n".format(func.__name__ if hasattr(func, "__name__") else func)) - future = asyncio.run_coroutine_threadsafe(wrapped_coroutine, self.conn.loop) - - if is_cancellable == CancelType.CANCELLABLE_ACTION: - def user_cancelled_action(fut): - if action_id is None: - return - - if fut.cancelled(): - self._abort_action(action_id) - - future.add_done_callback(user_cancelled_action) - - if is_cancellable == CancelType.CANCELLABLE_BEHAVIOR: - def user_cancelled_behavior(fut): - if fut.cancelled(): - self._abort_behavior() - - future.add_done_callback(user_cancelled_behavior) - - if requires_control: - self.conn.active_commands.append(future) - - def clear_when_done(fut): - if fut in self.conn.active_commands: - self.conn.active_commands.remove(fut) - future.add_done_callback(clear_when_done) - if _return_future: - return future - try: - return future.result() - except futures.CancelledError: - self.logger.warning(f"{func.__name__} cancelled because behavior control was lost") - return None - return result - return _on_connection_thread_decorator +# Copyright (c) 2018 Anki, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License in the file LICENSE.txt or at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Management of the connection to and from Vector. +""" + +# __all__ should order by constants, event classes, other classes, functions. +__all__ = ['ControlPriorityLevel', 'Connection', 'on_connection_thread'] + +import asyncio +from concurrent import futures +from enum import Enum +import functools +import inspect +import logging +import platform +import sys +import threading +from typing import Any, Awaitable, Callable, Coroutine, Dict, List + +from google.protobuf.text_format import MessageToString +import grpc +import aiogrpc + +from . import util +from .escapepod import EscapePod +from .exceptions import (connection_error, + VectorAsyncException, + VectorBehaviorControlException, + VectorConfigurationException, + VectorControlException, + VectorControlTimeoutException, + VectorInvalidVersionException, + VectorNotFoundException) +from .messaging import client, protocol +from .version import __version__ + + +class CancelType(Enum): + """Enum used to specify cancellation options for behaviors -- internal use only """ + #: Cancellable as an 'Action' + CANCELLABLE_ACTION = 0 + #: Cancellable as a 'Behavior' + CANCELLABLE_BEHAVIOR = 1 + + +class ControlPriorityLevel(Enum): + """Enum used to specify the priority level for the program.""" + #: Runs above mandatory physical reactions, will drive off table, perform while on a slope, + #: ignore low battery state, work in the dark, etc. + OVERRIDE_BEHAVIORS_PRIORITY = protocol.ControlRequest.OVERRIDE_BEHAVIORS # pylint: disable=no-member + #: Runs below Mandatory Physical Reactions such as tucking Vector's head and arms during a fall, + #: yet above Trigger-Word Detection. Default for normal operation. + DEFAULT_PRIORITY = protocol.ControlRequest.DEFAULT # pylint: disable=no-member + #: Holds control of robot before/after other SDK connections + #: Used to disable idle behaviors. Not to be used for regular behavior control. + RESERVE_CONTROL = protocol.ControlRequest.RESERVE_CONTROL # pylint: disable=no-member + + +class _ControlEventManager: + """This manages every :class:`asyncio.Event` that handles the behavior control + system. + + These include three events: granted, lost, and request. + + :class:`granted_event` represents the behavior system handing control to the SDK. + + :class:`lost_event` represents a higher priority behavior taking control away from the SDK. + + :class:`request_event` Is a way of alerting :class:`Connection` to request control. + """ + + def __init__(self, loop: asyncio.BaseEventLoop = None, priority: ControlPriorityLevel = None): + self._granted_event = asyncio.Event(loop=loop) + self._lost_event = asyncio.Event(loop=loop) + self._request_event = asyncio.Event(loop=loop) + self._has_control = False + self._priority = priority + self._is_shutdown = False + + @property + def granted_event(self) -> asyncio.Event: + """This event is used to notify listeners that control has been granted to the SDK.""" + return self._granted_event + + @property + def lost_event(self) -> asyncio.Event: + """Represents a higher priority behavior taking control away from the SDK.""" + return self._lost_event + + @property + def request_event(self) -> asyncio.Event: + """Used to alert :class:`Connection` to request control.""" + return self._request_event + + @property + def has_control(self) -> bool: + """Check to see that the behavior system has control (without blocking by checking :class:`granted_event`)""" + return self._has_control + + @property + def priority(self) -> ControlPriorityLevel: + """The currently desired priority for the SDK.""" + return self._priority + + @property + def is_shutdown(self) -> bool: + """Detect if the behavior control stream is supposed to shut down.""" + return self._is_shutdown + + def request(self, priority: ControlPriorityLevel = ControlPriorityLevel.DEFAULT_PRIORITY) -> None: + """Tell the behavior stream to request control via setting the :class:`request_event`. + + This will signal Connection's :func:`_request_handler` generator to send a request control message on the BehaviorControl stream. + This signal happens asynchronously, and can be tracked using the :class:`granted_event` parameter. + + :param priority: The level of control in the behavior system. This determines which actions are allowed to + interrupt the SDK execution. See :class:`ControlPriorityLevel` for more information. + """ + if priority is None: + raise VectorBehaviorControlException("Must provide a priority level to request. To disable control, use {}.release().", self.__class__.__name__) + self._priority = priority + self._request_event.set() + + def release(self) -> None: + """Tell the behavior stream to release control via setting the :class:`request_event` while priority is ``None``. + + This will signal Connection's :func:`_request_handler` generator to send a release control message on the BehaviorControl stream. + This signal happens asynchronously, and can be tracked using the :class:`lost_event` parameter. + """ + self._priority = None + self._request_event.set() + + def update(self, enabled: bool) -> None: + """Update the current state of control (either enabled or disabled) + + :param enabled: Used to enable/disable behavior control + """ + self._has_control = enabled + if enabled: + self._granted_event.set() + self._lost_event.clear() + else: + self._lost_event.set() + self._granted_event.clear() + + def shutdown(self) -> None: + """Tells the control stream to shut down. + + This will return control to the rest of the behavior system. + """ + self._has_control = False + self._granted_event.set() + self._lost_event.set() + self._is_shutdown = True + self._request_event.set() + + +class Connection: + """Creates and maintains a aiogrpc connection including managing the connection thread. + The connection thread decouples the actual messaging layer from the user's main thread, + and requires any network requests to be ran using :func:`asyncio.run_coroutine_threadsafe` + to make them run on the other thread. Connection provides two helper functions for running + a function on the connection thread: :func:`~Connection.run_coroutine` and + :func:`~Connection.run_soon`. + + This class may be used to bypass the structures of the python sdk handled by + :class:`~anki_vector.robot.Robot`, and instead talk to aiogrpc more directly. + + The values for the cert_file location and the guid can be found in your home directory in + the sdk_config.ini file. + + .. code-block:: python + + import anki_vector + + # Connect to your Vector + conn = anki_vector.connection.Connection("Vector-XXXX", "XX.XX.XX.XX:443", "/path/to/file.cert", "") + conn.connect() + # Run your commands + async def play_animation(): + # Run your commands + anim = anki_vector.messaging.protocol.Animation(name="anim_pounce_success_02") + anim_request = anki_vector.messaging.protocol.PlayAnimationRequest(animation=anim) + return await conn.grpc_interface.PlayAnimation(anim_request) # This needs to be run in an asyncio loop + conn.run_coroutine(play_animation()).result() + # Close the connection + conn.close() + + :param name: Vector's name in the format of "Vector-XXXX". + :param host: The IP address and port of Vector in the format "XX.XX.XX.XX:443". + :param cert_file: The location of the certificate file on disk. + :param guid: Your robot's unique secret key. + :param behavior_control_level: pass one of :class:`ControlPriorityLevel` priority levels if the connection + requires behavior control, or None to decline control. + """ + + def __init__(self, name: str, host: str, cert_file: str, guid: str, escape_pod: bool = False, behavior_control_level: ControlPriorityLevel = ControlPriorityLevel.DEFAULT_PRIORITY): + self._loop: asyncio.BaseEventLoop = None + self.name = name + self.host = host + self.cert_file = cert_file + self._escape_pod = escape_pod + self._interface = None + self._channel = None + self._has_control = False + self._logger = util.get_class_logger(__name__, self) + self._control_stream_task = None + self._control_events: _ControlEventManager = None + self._guid = guid + self._thread: threading.Thread = None + self._ready_signal: threading.Event = threading.Event() + self._done_signal: asyncio.Event = None + self._conn_exception = False + self._behavior_control_level = behavior_control_level + self.active_commands = [] + + @property + def loop(self) -> asyncio.BaseEventLoop: + """A direct reference to the loop on the connection thread. + Can be used to run functions in on thread. + + .. testcode:: + + import anki_vector + import asyncio + + async def connection_function(): + print("I'm running in the connection thread event loop.") + + with anki_vector.Robot() as robot: + asyncio.run_coroutine_threadsafe(connection_function(), robot.conn.loop) + + :returns: The loop running inside the connection thread + """ + if self._loop is None: + raise VectorAsyncException("Attempted to access the connection loop before it was ready") + return self._loop + + @property + def thread(self) -> threading.Thread: + """A direct reference to the connection thread. Available to callers to determine if the + current thread is the connection thread. + + .. testcode:: + + import anki_vector + import threading + + with anki_vector.Robot() as robot: + if threading.current_thread() is robot.conn.thread: + print("This code is running on the connection thread") + else: + print("This code is not running on the connection thread") + + :returns: The connection thread where all of the grpc messages are being processed. + """ + if self._thread is None: + raise VectorAsyncException("Attempted to access the connection loop before it was ready") + return self._thread + + @property + def grpc_interface(self) -> client.ExternalInterfaceStub: + """A direct reference to the connected aiogrpc interface. + + This may be used to directly call grpc messages bypassing :class:`anki_vector.Robot` + + .. code-block:: python + + import anki_vector + + # Connect to your Vector + conn = anki_vector.connection.Connection("Vector-XXXX", "XX.XX.XX.XX:443", "/path/to/file.cert", "") + conn.connect() + # Run your commands + async def play_animation(): + # Run your commands + anim = anki_vector.messaging.protocol.Animation(name="anim_pounce_success_02") + anim_request = anki_vector.messaging.protocol.PlayAnimationRequest(animation=anim) + return await conn.grpc_interface.PlayAnimation(anim_request) # This needs to be run in an asyncio loop + conn.run_coroutine(play_animation()).result() + # Close the connection + conn.close() + """ + return self._interface + + @property + def behavior_control_level(self) -> ControlPriorityLevel: + """Returns the specific :class:`ControlPriorityLevel` requested for behavior control. + + To be able to directly control Vector's motors, override his screen, play an animation, etc., + the :class:`Connection` will need behavior control. This property identifies the enumerated + level of behavior control that the SDK will maintain over the robot. + + For more information about behavior control, see :ref:`behavior `. + + .. code-block:: python + + import anki_vector + + with anki_vector.Robot() as robot: + print(robot.conn.behavior_control_level) # Will print ControlPriorityLevel.DEFAULT_PRIORITY + robot.conn.release_control() + print(robot.conn.behavior_control_level) # Will print None + """ + return self._behavior_control_level + + @property + def requires_behavior_control(self) -> bool: + """True if the :class:`Connection` requires behavior control. + + To be able to directly control Vector's motors, override his screen, play an animation, etc., + the :class:`Connection` will need behavior control. This boolean signifies that + the :class:`Connection` will try to maintain control of Vector's behavior system even after losing + control to higher priority robot behaviors such as returning home to charge a low battery. + + For more information about behavior control, see :ref:`behavior `. + + .. code-block:: python + + import time + + import anki_vector + + def callback(robot, event_type, event): + robot.conn.request_control() + print(robot.conn.requires_behavior_control) # Will print True + robot.anim.play_animation_trigger('GreetAfterLongTime') + robot.conn.release_control() + + with anki_vector.Robot(behavior_control_level=None) as robot: + print(robot.conn.requires_behavior_control) # Will print False + robot.events.subscribe(callback, anki_vector.events.Events.robot_observed_face) + + # Waits 10 seconds. Show Vector your face. + time.sleep(10) + """ + return self._behavior_control_level is not None + + @property + def control_lost_event(self) -> asyncio.Event: + """This provides an :class:`asyncio.Event` that a user may :func:`wait()` upon to + detect when Vector has taken control of the behavior system at a higher priority. + + .. testcode:: + + import anki_vector + + async def auto_reconnect(conn: anki_vector.connection.Connection): + await conn.control_lost_event.wait() + conn.request_control() + """ + return self._control_events.lost_event + + @property + def control_granted_event(self) -> asyncio.Event: + """This provides an :class:`asyncio.Event` that a user may :func:`wait()` upon to + detect when Vector has given control of the behavior system to the SDK program. + + .. testcode:: + + import anki_vector + + async def wait_for_control(conn: anki_vector.connection.Connection): + await conn.control_granted_event.wait() + # Run commands that require behavior control + """ + return self._control_events.granted_event + + def request_control(self, behavior_control_level: ControlPriorityLevel = ControlPriorityLevel.DEFAULT_PRIORITY, timeout: float = 10.0): + """Explicitly request behavior control. Typically used after detecting :func:`control_lost_event`. + + To be able to directly control Vector's motors, override his screen, play an animation, etc., + the :class:`Connection` will need behavior control. This function will acquire control + of Vector's behavior system. This will raise a :class:`VectorControlTimeoutException` if it fails + to gain control before the timeout. + + For more information about behavior control, see :ref:`behavior ` + + .. testcode:: + + import anki_vector + + async def auto_reconnect(conn: anki_vector.connection.Connection): + await conn.control_lost_event.wait() + conn.request_control(timeout=5.0) + + :param timeout: The time allotted to attempt a connection, in seconds. + :param behavior_control_level: request control of Vector's behavior system at a specific level of control. + See :class:`ControlPriorityLevel` for more information. + """ + if not isinstance(behavior_control_level, ControlPriorityLevel): + raise TypeError("behavior_control_level must be of type ControlPriorityLevel") + if self._thread is threading.current_thread(): + return asyncio.ensure_future(self._request_control(behavior_control_level=behavior_control_level, timeout=timeout), loop=self._loop) + return self.run_coroutine(self._request_control(behavior_control_level=behavior_control_level, timeout=timeout)) + + async def _request_control(self, behavior_control_level: ControlPriorityLevel = ControlPriorityLevel.DEFAULT_PRIORITY, timeout: float = 10.0): + self._behavior_control_level = behavior_control_level + self._control_events.request(self._behavior_control_level) + try: + self._has_control = await asyncio.wait_for(self.control_granted_event.wait(), timeout) + except futures.TimeoutError as e: + raise VectorControlTimeoutException(f"Surpassed timeout of {timeout}s") from e + + def release_control(self, timeout: float = 10.0): + """Explicitly release control. Typically used after detecting :func:`control_lost_event`. + + To be able to directly control Vector's motors, override his screen, play an animation, etc., + the :class:`Connection` will need behavior control. This function will release control + of Vector's behavior system. This will raise a :class:`VectorControlTimeoutException` if it fails + to receive a control_lost event before the timeout. + + .. testcode:: + + import anki_vector + + async def wait_for_control(conn: anki_vector.connection.Connection): + await conn.control_granted_event.wait() + # Run commands that require behavior control + conn.release_control() + + :param timeout: The time allotted to attempt to release control, in seconds. + """ + if self._thread is threading.current_thread(): + return asyncio.ensure_future(self._release_control(timeout=timeout), loop=self._loop) + return self.run_coroutine(self._release_control(timeout=timeout)) + + async def _release_control(self, timeout: float = 10.0): + self._behavior_control_level = None + self._control_events.release() + try: + self._has_control = await asyncio.wait_for(self.control_lost_event.wait(), timeout) + except futures.TimeoutError as e: + raise VectorControlTimeoutException(f"Surpassed timeout of {timeout}s") from e + + def connect(self, timeout: float = 10.0) -> None: + """Connect to Vector. This will start the connection thread which handles all messages + between Vector and Python. + + .. code-block:: python + + import anki_vector + + # Connect to your Vector + conn = anki_vector.connection.Connection("Vector-XXXX", "XX.XX.XX.XX:443", "/path/to/file.cert", "") + conn.connect() + # Run your commands + async def play_animation(): + # Run your commands + anim = anki_vector.messaging.protocol.Animation(name="anim_pounce_success_02") + anim_request = anki_vector.messaging.protocol.PlayAnimationRequest(animation=anim) + return await conn.grpc_interface.PlayAnimation(anim_request) # This needs to be run in an asyncio loop + conn.run_coroutine(play_animation()).result() + # Close the connection + conn.close() + + :param timeout: The time allotted to attempt a connection, in seconds. + """ + if self._thread: + raise VectorAsyncException("\n\nRepeated connections made to open Connection.") + self._ready_signal.clear() + self._thread = threading.Thread(target=self._connect, args=(timeout,), daemon=True, name="gRPC Connection Handler Thread") + self._thread.start() + ready = self._ready_signal.wait(timeout=4 * timeout) + if not ready: + raise VectorNotFoundException() + if hasattr(self._ready_signal, "exception"): + e = getattr(self._ready_signal, "exception") + delattr(self._ready_signal, "exception") + raise e + + def _connect(self, timeout: float) -> None: + """The function that runs on the connection thread. This will connect to Vector, + and establish the BehaviorControl stream. + """ + try: + if threading.main_thread() is threading.current_thread(): + raise VectorAsyncException("\n\nConnection._connect must be run outside of the main thread.") + self._loop = asyncio.new_event_loop() + asyncio.set_event_loop(self._loop) + self._done_signal = asyncio.Event() + if not self._behavior_control_level: + self._control_events = _ControlEventManager(self._loop) + else: + self._control_events = _ControlEventManager(self._loop, priority=self._behavior_control_level) + + trusted_certs = None + if not self.cert_file is None: + with open(self.cert_file, 'rb') as cert: + trusted_certs = cert.read() + else: + if not self._escape_pod: + raise VectorConfigurationException("Must provide a cert file to authenticate to Vector.") + + if self._escape_pod: + if not EscapePod.validate_certificate_name(self.cert_file, self.name): + trusted_certs = EscapePod.get_authentication_certificate(self.host) + self.name = EscapePod.get_certificate_name(trusted_certs) + self._guid = EscapePod.authenticate_escape_pod(self.host, self.name, trusted_certs) + + # Pin the robot certificate for opening the channel + channel_credentials = aiogrpc.ssl_channel_credentials(root_certificates=trusted_certs) + # Add authorization header for all the calls + call_credentials = aiogrpc.access_token_call_credentials(self._guid) + + credentials = aiogrpc.composite_channel_credentials(channel_credentials, call_credentials) + + self._logger.info(f"Connecting to {self.host} for {self.name} using {self.cert_file}") + self._channel = aiogrpc.secure_channel(self.host, credentials, + options=(("grpc.ssl_target_name_override", self.name,),)) + + # Verify the connection to Vector is able to be established (client-side) + try: + # Explicitly grab _channel._channel to test the underlying grpc channel directly + grpc.channel_ready_future(self._channel._channel).result(timeout=timeout) # pylint: disable=protected-access + except grpc.FutureTimeoutError as e: + raise VectorNotFoundException() from e + + self._interface = client.ExternalInterfaceStub(self._channel) + + # Verify Vector and the SDK have compatible protocol versions + version = protocol.ProtocolVersionRequest(client_version=protocol.PROTOCOL_VERSION_CURRENT, min_host_version=protocol.PROTOCOL_VERSION_MINIMUM) + protocol_version = self._loop.run_until_complete(self._interface.ProtocolVersion(version)) + if protocol_version.result != protocol.ProtocolVersionResponse.SUCCESS or protocol.PROTOCOL_VERSION_MINIMUM > protocol_version.host_version: # pylint: disable=no-member + raise VectorInvalidVersionException(protocol_version) + + self._control_stream_task = self._loop.create_task(self._open_connections()) + + # Initialze SDK + sdk_module_version = __version__ + python_version = platform.python_version() + python_implementation = platform.python_implementation() + os_version = platform.platform() + cpu_version = platform.machine() + initialize = protocol.SDKInitializationRequest(sdk_module_version=sdk_module_version, + python_version=python_version, + python_implementation=python_implementation, + os_version=os_version, + cpu_version=cpu_version) + self._loop.run_until_complete(self._interface.SDKInitialization(initialize)) + + if self._behavior_control_level: + self._loop.run_until_complete(self._request_control(behavior_control_level=self._behavior_control_level, timeout=timeout)) + except Exception as e: # pylint: disable=broad-except + # Propagate the errors to the calling thread + setattr(self._ready_signal, "exception", e) + self._loop.close() + return + finally: + self._ready_signal.set() + + try: + async def wait_until_done(): + return await self._done_signal.wait() + self._loop.run_until_complete(wait_until_done()) + finally: + self._loop.close() + + async def _request_handler(self): + """Handles generating messages for the BehaviorControl stream.""" + while await self._control_events.request_event.wait(): + self._control_events.request_event.clear() + if self._control_events.is_shutdown: + return + priority = self._control_events.priority + if priority is None: + msg = protocol.ControlRelease() + msg = protocol.BehaviorControlRequest(control_release=msg) + else: + msg = protocol.ControlRequest(priority=priority.value) + msg = protocol.BehaviorControlRequest(control_request=msg) + self._logger.debug(f"BehaviorControl {MessageToString(msg, as_one_line=True)}") + yield msg + + async def _open_connections(self): + """Starts the BehaviorControl stream, and handles the messages coming back from the robot.""" + try: + async for response in self._interface.BehaviorControl(self._request_handler()): + response_type = response.WhichOneof("response_type") + if response_type == 'control_granted_response': + self._logger.info(f"BehaviorControl {MessageToString(response, as_one_line=True)}") + self._control_events.update(True) + elif response_type == 'control_lost_event': + self._cancel_active() + self._logger.info(f"BehaviorControl {MessageToString(response, as_one_line=True)}") + self._control_events.update(False) + except futures.CancelledError: + self._logger.debug('Behavior handler task was cancelled. This is expected during disconnection.') + + def _cancel_active(self): + for fut in self.active_commands: + if not fut.done(): + fut.cancel() + self.active_commands = [] + + def close(self): + """Cleanup the connection, and shutdown all the event handlers. + + Usually this should be invoked by the Robot class when it closes. + + .. code-block:: python + + import anki_vector + + # Connect to your Vector + conn = anki_vector.connection.Connection("Vector-XXXX", "XX.XX.XX.XX:443", "/path/to/file.cert", "") + conn.connect() + # Run your commands + async def play_animation(): + # Run your commands + anim = anki_vector.messaging.protocol.Animation(name="anim_pounce_success_02") + anim_request = anki_vector.messaging.protocol.PlayAnimationRequest(animation=anim) + return await conn.grpc_interface.PlayAnimation(anim_request) # This needs to be run in an asyncio loop + conn.run_coroutine(play_animation()).result() + # Close the connection + conn.close() + """ + if self._control_events: + self._control_events.shutdown() + if self._control_stream_task: + self._control_stream_task.cancel() + self.run_coroutine(self._control_stream_task).result() + self._cancel_active() + if self._channel: + self.run_coroutine(self._channel.close()).result() + self.run_coroutine(self._done_signal.set) + self._thread.join(timeout=5) + self._thread = None + + def run_soon(self, coro: Awaitable) -> None: + """Schedules the given awaitable to run on the event loop for the connection thread. + + .. testcode:: + + import anki_vector + import time + + async def my_coroutine(): + print("Running on the connection thread") + + with anki_vector.Robot() as robot: + robot.conn.run_soon(my_coroutine()) + time.sleep(1) + + :param coro: The coroutine, task or any awaitable to schedule for execution on the connection thread. + """ + if coro is None or not inspect.isawaitable(coro): + raise VectorAsyncException(f"\n\n{coro.__name__ if hasattr(coro, '__name__') else coro} is not awaitable, so cannot be ran with run_soon.\n") + + def soon(): + try: + asyncio.ensure_future(coro) + except TypeError as e: + raise VectorAsyncException(f"\n\n{coro.__name__ if hasattr(coro, '__name__') else coro} could not be ensured as a future.\n") from e + if threading.current_thread() is self._thread: + self._loop.call_soon(soon) + else: + self._loop.call_soon_threadsafe(soon) + + def run_coroutine(self, coro: Awaitable) -> Any: + """Runs a given awaitable on the connection thread's event loop. + Cannot be called from within the connection thread. + + .. testcode:: + + import anki_vector + + async def my_coroutine(): + print("Running on the connection thread") + return "Finished" + + with anki_vector.Robot() as robot: + result = robot.conn.run_coroutine(my_coroutine()) + + :param coro: The coroutine, task or any other awaitable which should be executed. + :returns: The result of the awaitable's execution. + """ + if threading.current_thread() is self._thread: + raise VectorAsyncException("Attempting to invoke async from same thread." + "Instead you may want to use 'run_soon'") + if asyncio.iscoroutinefunction(coro) or asyncio.iscoroutine(coro): + return self._run_coroutine(coro) + if asyncio.isfuture(coro): + async def future_coro(): + return await coro + return self._run_coroutine(future_coro()) + if callable(coro): + async def wrapped_coro(): + return coro() + return self._run_coroutine(wrapped_coro()) + raise VectorAsyncException("\n\nInvalid parameter to run_coroutine: {}\n" + "This function expects a coroutine, task, or awaitable.".format(type(coro))) + + def _run_coroutine(self, coro): + return asyncio.run_coroutine_threadsafe(coro, self._loop) + + +def on_connection_thread(log_messaging: bool = True, requires_control: bool = True, is_cancellable: CancelType = None) -> Callable[[Coroutine[util.Component, Any, None]], Any]: + """A decorator generator used internally to denote which functions will run on + the connection thread. This unblocks the caller of the wrapped function + and allows them to continue running while the messages are being processed. + + .. code-block:: python + + import anki_vector + + class MyComponent(anki_vector.util.Component): + @connection._on_connection_thread() + async def on_connection_thread(self): + # Do work on the connection thread + + :param log_messaging: True if the log output should include the entire message or just the size. Recommended for + large binary return values. + :param requires_control: True if the function should wait until behavior control is granted before executing. + :param is_cancellable: use a valid enum of :class:`CancelType` to specify the type of cancellation for the + function. Defaults to 'None' implying no support for responding to cancellation. + :returns: A decorator which has 3 possible returns based on context: the result of the decorated function, + the :class:`concurrent.futures.Future` which points to the decorated function, or the + :class:`asyncio.Future` which points to the decorated function. + These contexts are: when the robot is a :class:`~anki_vector.robot.Robot`, + when the robot is an :class:`~anki_vector.robot.AsyncRobot`, and when + called from the connection thread respectively. + """ + def _on_connection_thread_decorator(func: Coroutine) -> Any: + """A decorator which specifies a function to be executed on the connection thread + + :params func: The function to be decorated + :returns: There are 3 possible returns based on context: the result of the decorated function, + the :class:`concurrent.futures.Future` which points to the decorated function, or the + :class:`asyncio.Future` which points to the decorated function. + These contexts are: when the robot is a :class:`anki_vector.robot.Robot`, + when the robot is an :class:`anki_vector.robot.AsyncRobot`, and when + called from the connection thread respectively. + """ + if not asyncio.iscoroutinefunction(func): + raise VectorAsyncException("\n\nCannot define non-coroutine function '{}' to run on connection thread.\n" + "Make sure the function is defined using 'async def'.".format(func.__name__ if hasattr(func, "__name__") else func)) + + @functools.wraps(func) + async def log_handler(conn: Connection, func: Coroutine, logger: logging.Logger, *args: List[Any], **kwargs: Dict[str, Any]) -> Coroutine: + """Wrap the provided coroutine to better express exceptions as specific :class:`anki_vector.exceptions.VectorException`s, and + adds logging to incoming (from the robot) and outgoing (to the robot) messages. + """ + result = None + # TODO: only have the request wait for control if we're not done. If done raise an exception. + control = conn.control_granted_event + if requires_control and not control.is_set(): + if not conn.requires_behavior_control: + raise VectorControlException(func.__name__) + logger.info(f"Delaying {func.__name__} until behavior control is granted") + await asyncio.wait([conn.control_granted_event.wait()], timeout=10) + message = args[1:] + outgoing = message if log_messaging else "size = {} bytes".format(sys.getsizeof(message)) + logger.debug(f'Outgoing {func.__name__}: {outgoing}') + try: + result = await func(*args, **kwargs) + except grpc.RpcError as rpc_error: + raise connection_error(rpc_error) from rpc_error + incoming = str(result).strip() if log_messaging else "size = {} bytes".format(sys.getsizeof(result)) + logger.debug(f'Incoming {func.__name__}: {type(result).__name__} {incoming}') + return result + + @functools.wraps(func) + def result(*args: List[Any], **kwargs: Dict[str, Any]) -> Any: + """The function that is the result of the decorator. Provides a wrapped function. + + :param _return_future: A hidden parameter which allows the wrapped function to explicitly + return a future (default for AsyncRobot) or not (default for Robot). + :returns: Based on context this can return the result of the decorated function, + the :class:`concurrent.futures.Future` which points to the decorated function, or the + :class:`asyncio.Future` which points to the decorated function. + These contexts are: when the robot is a :class:`anki_vector.robot.Robot`, + when the robot is an :class:`anki_vector.robot.AsyncRobot`, and when + called from the connection thread respectively.""" + self = args[0] # Get the self reference from the function call + # if the call supplies a _return_future parameter then override force_async with that. + _return_future = kwargs.pop('_return_future', self.force_async) + + action_id = None + if is_cancellable == CancelType.CANCELLABLE_ACTION: + action_id = self._get_next_action_id() + kwargs['_action_id'] = action_id + + wrapped_coroutine = log_handler(self.conn, func, self.logger, *args, **kwargs) + + if threading.current_thread() == self.conn.thread: + if self.conn.loop.is_running(): + return asyncio.ensure_future(wrapped_coroutine, loop=self.conn.loop) + raise VectorAsyncException("\n\nThe connection thread loop is not running, but a " + "function '{}' is being invoked on that thread.\n".format(func.__name__ if hasattr(func, "__name__") else func)) + future = asyncio.run_coroutine_threadsafe(wrapped_coroutine, self.conn.loop) + + if is_cancellable == CancelType.CANCELLABLE_ACTION: + def user_cancelled_action(fut): + if action_id is None: + return + + if fut.cancelled(): + self._abort_action(action_id) + + future.add_done_callback(user_cancelled_action) + + if is_cancellable == CancelType.CANCELLABLE_BEHAVIOR: + def user_cancelled_behavior(fut): + if fut.cancelled(): + self._abort_behavior() + + future.add_done_callback(user_cancelled_behavior) + + if requires_control: + self.conn.active_commands.append(future) + + def clear_when_done(fut): + if fut in self.conn.active_commands: + self.conn.active_commands.remove(fut) + future.add_done_callback(clear_when_done) + if _return_future: + return future + try: + return future.result() + except futures.CancelledError: + self.logger.warning(f"{func.__name__} cancelled because behavior control was lost") + return None + return result + return _on_connection_thread_decorator diff --git a/anki_vector/escapepod.py b/anki_vector/escapepod.py new file mode 100644 index 0000000..b67f7cb --- /dev/null +++ b/anki_vector/escapepod.py @@ -0,0 +1,171 @@ +# Copyright (c) 2021 cyb3rdog +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License in the file LICENSE.txt or at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import grpc +import json +import ssl +import socket +import requests +import base64 +import urllib3 +urllib3.disable_warnings() + +from cryptography import x509 +from cryptography.hazmat.backends import default_backend + +from os import name + +from . import messaging +from .exceptions import (connection_error, + VectorUnauthenticatedException, + VectorNotFoundException) + +class EscapePodMetadataPlugin: + """A specification for custom escape pod authentication.""" + + def __init__(self, token_guid): + self.token_hash = token_guid + + def __call__(self, context, callback): + """Implements authentication by passing metadata to a callback. + Args: + access_token: A string to place directly in the http request + authorization header, for example + "authorization: Bearer ". + callback: An EscapePodMetadataPlugin to be invoked either + synchronously or asynchronously. + """ + + callback((('authorization', 'Bearer ' + self.token_hash ),), None) + + +class EscapePod: + + def url_for(host, method): + return "https://{}/v1/{}".format(host, method) + + # Make a request and return a pair (status_code,result_json) + # result_json is null if the call fails + def request(host, guid, method, request_json): + url = EscapePod.url_for( host, method ) + auth = 'Bearer ' + guid + headers = {"Authorization": auth, "Content-Type" : "application/json" } + try: + response = requests.post( url, json=request_json, headers=headers, stream=False, + # cert = self._cfg['cert'] ) + verify=False ) + except requests.exceptions.ConnectionError as e: + raise VectorNotFoundException() from e + except requests.exceptions.Timeout as e: + raise VectorNotFoundException() from e + except requests.exceptions.RequestException as e: + raise VectorNotFoundException() from e + + if response.status_code == 200: + return (response.status_code, response.json()) + + return (response.status_code, None) + + # + # The new DDL's Go-SDK uses grpc insecure channel together with call credentials. + # That kind of credentials composition is not supported on low-level of many grpc + # implementations including the python's grpc core. The reason for that is to + # discourage developers from sending the credentials over the unecrypted channels. + # This method is a replication of the Go-SDK current authentication method and is + # not being used by this SDK. For the reference and educational purposes only. + # + def authenticate_insecure(host: str, name:str, cert: bytes = None) -> str: + + guid = base64.b64encode( bytes("Anything1", 'utf-8')).decode('utf-8') + request_json = {"user_session_id": guid } + response = EscapePod.request( host, guid, 'user_authentication', request_json ) + if response[1]: + guid = base64.b64decode(response[1]['client_token_guid']).decode( 'utf-8' ) + return guid + else: + print( response[0] ) + + return None + + + def authenticate_escape_pod(host: str, name: str, cert: bytes = None) -> str: + """Authenticates the escape pod Vector and returns the token hash guid""" + + call_credentials = grpc.metadata_call_credentials(EscapePodMetadataPlugin('Anything1'), + name='authorization') + # Channel credential will be valid for the entire channel + channel_credential = grpc.ssl_channel_credentials(root_certificates=cert) + + # Combining channel credentials and call credentials together + credentials = grpc.composite_channel_credentials( + channel_credential, + call_credentials, + ) + + channel = grpc.secure_channel(host, credentials, options=(("grpc.ssl_target_name_override", name,),)) + + # Verify the connection to Vector is able to be established (client-side) + try: + # Explicitly grab _channel._channel to test the underlying grpc channel directly + grpc.channel_ready_future(channel).result(timeout=15) + except grpc.FutureTimeoutError as e: + raise VectorNotFoundException() from e + + try: + interface = messaging.client.ExternalInterfaceStub(channel) + request = messaging.protocol.UserAuthenticationRequest( + user_session_id='anything1'.encode('utf-8'), + client_name='anything2'.encode('utf-8')) + + response = interface.UserAuthentication(request) + if response.code != messaging.protocol.UserAuthenticationResponse.AUTHORIZED: # pylint: disable=no-member + raise VectorUnauthenticatedException('Failed to authenticate') + except grpc.RpcError as e: + raise VectorUnauthenticatedException() from e + + return response.client_token_guid.decode("utf-8") + + + def get_authentication_certificate(hostname:str) -> str: + """Get the Vector gateway certificate""" + host = hostname.split(":")[0] + port = int(hostname.split(":")[1] or 443) + conn = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + context = ssl.SSLContext(ssl.PROTOCOL_SSLv23) + sock = context.wrap_socket(conn, server_hostname=host) + sock.connect((host, port)) + cert = ssl.DER_cert_to_PEM_cert(sock.getpeercert(True)) + return str.encode(cert) + + + def get_certificate_name(cert_data) -> str: + """Validate the name on Vector's certificate against the user-provided name""" + if cert_data is None: + return None + + cert = x509.load_pem_x509_certificate(cert_data, default_backend()) + for fields in cert.subject: + current = str(fields.oid) + if "commonName" in current: + return fields.value + + + def validate_certificate_name(cert_file, robot_name) -> bool: + """Validate the name on Vector's certificate against the user-provided name""" + if cert_file is None or robot_name is None: + return False + + with open(cert_file, "rb") as f: + cert_data = f.read() + return EscapePod.get_certificate_name(cert_data) == robot_name \ No newline at end of file diff --git a/anki_vector/robot.py b/anki_vector/robot.py index 4aaadec..530b830 100755 --- a/anki_vector/robot.py +++ b/anki_vector/robot.py @@ -1,928 +1,930 @@ -# Copyright (c) 2018 Anki, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License in the file LICENSE.txt or at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -This contains the :class:`Robot` and :class:`AsyncRobot` classes for managing Vector. - -:class:`Robot` will run all behaviors in sequence and directly return the results. - -:class:`AsyncRobot` will instead provide a :class:`concurrent.futures.Future` which the -caller may use to obtain the result when they desire. -""" - -# __all__ should order by constants, event classes, other classes, functions. -__all__ = ['Robot', 'AsyncRobot'] - -import concurrent -import functools - -from . import (animation, audio, behavior, camera, - events, faces, motors, nav_map, screen, - photos, proximity, status, touch, - util, viewer, vision, world) -from .connection import (Connection, - on_connection_thread, - ControlPriorityLevel) -from .exceptions import (VectorNotReadyException, - VectorPropertyValueNotReadyException, - VectorUnreliableEventStreamException) -from .viewer import (ViewerComponent, Viewer3DComponent) -from .messaging import protocol -from .mdns import VectorMdns - - -class Robot: - """The Robot object is responsible for managing the state and connections - to a Vector, and is typically the entry-point to running the sdk. - - The majority of the robot will not work until it is properly connected - to Vector. There are two ways to get connected: - - 1. Using :code:`with`: it works just like opening a file, and will close when - the :code:`with` block's indentation ends. - - - .. testcode:: - - import anki_vector - - # Create the robot connection - with anki_vector.Robot() as robot: - # Run your commands - robot.anim.play_animation_trigger("GreetAfterLongTime") - - 2. Using :func:`connect` and :func:`disconnect` to explicitly open and close the connection: - it allows the robot's connection to continue in the context in which it started. - - .. testcode:: - - import anki_vector - - # Create a Robot object - robot = anki_vector.Robot() - # Connect to the Robot - robot.connect() - # Run your commands - robot.anim.play_animation_trigger("GreetAfterLongTime") - # Disconnect from Vector - robot.disconnect() - - :param serial: Vector's serial number. The robot's serial number (ex. 00e20100) is located on the underside of Vector, - or accessible from Vector's debug screen. Used to identify which Vector configuration to load. - :param ip: Vector's IP address. (optional) - :param name: Vector's name (in format :code:`"Vector-XXXX"`) to be used for mDNS discovery. If a Vector with the given name - is discovered, the :code:`ip` parameter (and config field) will be overridden. - :param config: A custom :class:`dict` to override values in Vector's configuration. (optional) - Example: :code:`{"cert": "/path/to/file.cert", "name": "Vector-XXXX", "guid": ""}` - where :code:`cert` is the certificate to identify Vector, :code:`name` is the name on Vector's face - when his backpack is double-clicked on the charger, and :code:`guid` is the authorization token - that identifies the SDK user. Note: Never share your authentication credentials with anyone. - :param default_logging: Toggle default logging. - :param behavior_activation_timeout: The time to wait for control of the robot before failing. - :param cache_animation_lists: Get the list of animation triggers and animations available at startup. - :param enable_face_detection: Turn on face detection. - :param estimate_facial_expression: Turn estimating facial expression on/off. Enabling :code:`estimate_facial_expression` - returns a facial expression, the expression values and the :class:`anki_vector.util.ImageRect` - for observed face regions (eyes, nose, and mouth) as part of the :code:`RobotObservedFace` event. - It is turned off by default as the number of :code:`RobotObservedFace` events - are reduced due to the increased processing time. - :param enable_audio_feed: Turn audio feed on/off. - :param enable_custom_object_detection: Turn custom object detection on/off. - :param enable_nav_map_feed: Turn navigation map feed on/off. - :param show_viewer: Specifies whether to display a view of Vector's camera in a window. - :param show_3d_viewer: Specifies whether to display a 3D view of Vector's understanding of the world in a window. - :param behavior_control_level: Request control of Vector's behavior system at a specific level of control. Pass - :code:`None` if behavior control is not needed. - See :class:`ControlPriorityLevel` for more information.""" - - def __init__(self, - serial: str = None, - ip: str = None, - name: str = None, - config: dict = None, - default_logging: bool = True, - behavior_activation_timeout: int = 10, - cache_animation_lists: bool = True, - enable_face_detection: bool = False, - estimate_facial_expression: bool = False, - enable_audio_feed: bool = False, - enable_custom_object_detection: bool = False, - enable_nav_map_feed: bool = None, - show_viewer: bool = False, - show_3d_viewer: bool = False, - behavior_control_level: ControlPriorityLevel = ControlPriorityLevel.DEFAULT_PRIORITY): - if default_logging: - util.setup_basic_logging() - self.logger = util.get_class_logger(__name__, self) - self._force_async = False - config = config if config is not None else {} - config = {**util.read_configuration(serial, name, self.logger), **config} - - if name is not None: - vector_mdns = VectorMdns.find_vector(name) - - if vector_mdns is not None: - ip = vector_mdns['ipv4'] - - self._name = config["name"] - self._ip = ip if ip is not None else config["ip"] - self._cert_file = config["cert"] - self._guid = config["guid"] - - self._port = "443" - if 'port' in config: - self._port = config["port"] - - if self._name is None or self._ip is None or self._cert_file is None or self._guid is None: - raise ValueError("The Robot object requires a serial and for Vector to be logged in (using the app then running the anki_vector.configure executable submodule).\n" - "You may also provide the values necessary for connection through the config parameter. ex: " - '{"name":"Vector-XXXX", "ip":"XX.XX.XX.XX", "cert":"/path/to/cert_file", "guid":""}') - - #: :class:`anki_vector.connection.Connection`: The active connection to the robot. - self._conn = Connection(self._name, ':'.join([self._ip, self._port]), self._cert_file, self._guid, behavior_control_level=behavior_control_level) - self._events = events.EventHandler(self) - - # placeholders for components before they exist - self._anim: animation.AnimationComponent = None - self._audio: audio.AudioComponent = None - self._behavior: behavior.BehaviorComponent = None - self._camera: camera.CameraComponent = None - self._faces: faces.FaceComponent = None - self._motors: motors.MotorComponent = None - self._nav_map: nav_map.NavMapComponent = None - self._screen: screen.ScreenComponent = None - self._photos: photos.PhotographComponent = None - self._proximity: proximity.ProximityComponent = None - self._touch: touch.TouchComponent = None - self._viewer: viewer.ViewerComponent = None - self._viewer_3d: viewer.Viewer3DComponent = None - self._vision: vision.VisionComponent = None - self._world: world.World = None - - self.behavior_activation_timeout = behavior_activation_timeout - self.enable_face_detection = enable_face_detection - self.estimate_facial_expression = estimate_facial_expression - self.enable_custom_object_detection = enable_custom_object_detection - self.cache_animation_lists = cache_animation_lists - - # Robot state/sensor data - self._pose: util.Pose = None - self._pose_angle_rad: float = None - self._pose_pitch_rad: float = None - self._left_wheel_speed_mmps: float = None - self._right_wheel_speed_mmps: float = None - self._head_angle_rad: float = None - self._lift_height_mm: float = None - self._accel: util.Vector3 = None - self._gyro: util.Vector3 = None - self._carrying_object_id: float = None - self._head_tracking_object_id: float = None - self._localized_to_object_id: float = None - self._last_image_time_stamp: float = None - self._status: status.RobotStatus = status.RobotStatus() - - self._enable_audio_feed = enable_audio_feed - if enable_nav_map_feed is not None: - self._enable_nav_map_feed = enable_nav_map_feed - else: - self._enable_nav_map_feed = False - self._show_viewer = show_viewer - self._show_3d_viewer = show_3d_viewer - if show_3d_viewer and enable_nav_map_feed is None: - self.logger.warning("enable_nav_map_feed should be True for 3d viewer to render correctly.") - self._enable_nav_map_feed = True - - @property - def force_async(self) -> bool: - """A flag used to determine if this is a :class:`Robot` or :class:`AsyncRobot`.""" - return self._force_async - - @property - def conn(self) -> Connection: - """A reference to the :class:`~anki_vector.connection.Connection` instance.""" - return self._conn - - @property - def events(self) -> events.EventHandler: - """A reference to the :class:`~anki_vector.events.EventHandler` instance.""" - return self._events - - @property - def anim(self) -> animation.AnimationComponent: - """A reference to the :class:`~anki_vector.animation.AnimationComponent` instance.""" - if self._anim is None: - raise VectorNotReadyException("AnimationComponent is not yet initialized") - return self._anim - - @property - def audio(self) -> audio.AudioComponent: - """The audio instance used to control Vector's microphone feed and speaker playback.""" - - if self._audio is None: - raise VectorNotReadyException("AudioComponent is not yet initialized") - return self._audio - - @property - def behavior(self) -> behavior.BehaviorComponent: - """A reference to the :class:`~anki_vector.behavior.BehaviorComponent` instance.""" - return self._behavior - - @property - def camera(self) -> camera.CameraComponent: - """The :class:`~anki_vector.camera.CameraComponent` instance used to control Vector's camera feed. - - .. testcode:: - - import anki_vector - - with anki_vector.Robot() as robot: - robot.camera.init_camera_feed() - image = robot.camera.latest_image - image.raw_image.show() - """ - if self._camera is None: - raise VectorNotReadyException("CameraComponent is not yet initialized") - return self._camera - - @property - def faces(self) -> faces.FaceComponent: - """A reference to the :class:`~anki_vector.faces.FaceComponent` instance.""" - if self._faces is None: - raise VectorNotReadyException("FaceComponent is not yet initialized") - return self._faces - - @property - def motors(self) -> motors.MotorComponent: - """A reference to the :class:`~anki_vector.motors.MotorComponent` instance.""" - if self._motors is None: - raise VectorNotReadyException("MotorComponent is not yet initialized") - return self._motors - - @property - def nav_map(self) -> nav_map.NavMapComponent: - """A reference to the :class:`~anki_vector.nav_map.NavMapComponent` instance.""" - if self._nav_map is None: - raise VectorNotReadyException("NavMapComponent is not yet initialized") - return self._nav_map - - @property - def screen(self) -> screen.ScreenComponent: - """A reference to the :class:`~anki_vector.screen.ScreenComponent` instance.""" - if self._screen is None: - raise VectorNotReadyException("ScreenComponent is not yet initialized") - return self._screen - - @property - def photos(self) -> photos.PhotographComponent: - """A reference to the :class:`~anki_vector.photos.PhotographComponent` instance.""" - if self._photos is None: - raise VectorNotReadyException("PhotographyComponent is not yet initialized") - return self._photos - - @property - def proximity(self) -> proximity.ProximityComponent: - """:class:`~anki_vector.proximity.ProximityComponent` containing state related to object proximity detection. - - .. code-block:: python - - import anki_vector - with anki_vector.Robot() as robot: - proximity_data = robot.proximity.last_sensor_reading - if proximity_data is not None: - print(proximity_data.distance) - """ - return self._proximity - - @property - def touch(self) -> touch.TouchComponent: - """:class:`~anki_vector.touch.TouchComponent` containing state related to object touch detection. - - .. testcode:: - - import anki_vector - with anki_vector.Robot() as robot: - print('Robot is being touched: {0}'.format(robot.touch.last_sensor_reading.is_being_touched)) - """ - return self._touch - - @property - def viewer(self) -> ViewerComponent: - """The :class:`~anki_vector.viewer.ViewerComponent` instance used to render Vector's camera feed. - - .. testcode:: - - import time - - import anki_vector - - with anki_vector.Robot() as robot: - # Render video for 5 seconds - robot.viewer.show() - time.sleep(5) - - # Disable video render and camera feed for 5 seconds - robot.viewer.close() - """ - if self._viewer is None: - raise VectorNotReadyException("ViewerComponent is not yet initialized") - return self._viewer - - @property - def viewer_3d(self) -> Viewer3DComponent: - """The :class:`~anki_vector.viewer.Viewer3DComponent` instance used to render Vector's navigation map. - - .. testcode:: - - import time - - import anki_vector - - with anki_vector.Robot(show_3d_viewer=True, enable_nav_map_feed=True) as robot: - # Render 3D view of navigation map for 5 seconds - time.sleep(5) - """ - if self._viewer_3d is None: - raise VectorNotReadyException("Viewer3DComponent is not yet initialized") - return self._viewer_3d - - @property - def vision(self) -> vision.VisionComponent: - """:class:`~anki_vector.vision.VisionComponent` containing functionality related to vision based object detection. - - .. testcode:: - - import anki_vector - with anki_vector.Robot() as robot: - robot.vision.enable_custom_object_detection() - """ - return self._vision - - @property - def world(self) -> world.World: - """A reference to the :class:`~anki_vector.world.World` instance, or None if the World is not yet initialized.""" - if self._world is None: - raise VectorNotReadyException("WorldComponent is not yet initialized") - return self._world - - @property - @util.block_while_none() - def pose(self) -> util.Pose: - """:class:`anki_vector.util.Pose`: The current pose (position and orientation) of Vector. - - .. testcode:: - - import anki_vector - with anki_vector.Robot() as robot: - current_robot_pose = robot.pose - """ - return self._pose - - @property - @util.block_while_none() - def pose_angle_rad(self) -> float: - """Vector's pose angle (heading in X-Y plane). - - .. testcode:: - - import anki_vector - with anki_vector.Robot() as robot: - current_pose_angle_rad = robot.pose_angle_rad - """ - return self._pose_angle_rad - - @property - @util.block_while_none() - def pose_pitch_rad(self) -> float: - """Vector's pose pitch (angle up/down). - - .. testcode:: - - import anki_vector - with anki_vector.Robot() as robot: - current_pose_pitch_rad = robot.pose_pitch_rad - """ - return self._pose_pitch_rad - - @property - @util.block_while_none() - def left_wheel_speed_mmps(self) -> float: - """Vector's left wheel speed in mm/sec - - .. testcode:: - - import anki_vector - with anki_vector.Robot() as robot: - current_left_wheel_speed_mmps = robot.left_wheel_speed_mmps - """ - return self._left_wheel_speed_mmps - - @property - @util.block_while_none() - def right_wheel_speed_mmps(self) -> float: - """Vector's right wheel speed in mm/sec - - .. testcode:: - - import anki_vector - with anki_vector.Robot() as robot: - current_right_wheel_speed_mmps = robot.right_wheel_speed_mmps - """ - return self._right_wheel_speed_mmps - - @property - @util.block_while_none() - def head_angle_rad(self) -> float: - """Vector's head angle (up/down). - - .. testcode:: - - import anki_vector - with anki_vector.Robot() as robot: - current_head_angle_rad = robot.head_angle_rad - """ - return self._head_angle_rad - - @property - @util.block_while_none() - def lift_height_mm(self) -> float: - """Height of Vector's lift from the ground. - - .. testcode:: - - import anki_vector - with anki_vector.Robot() as robot: - current_lift_height_mm = robot.lift_height_mm - """ - return self._lift_height_mm - - @property - @util.block_while_none() - def accel(self) -> util.Vector3: - """:class:`anki_vector.util.Vector3`: The current accelerometer reading (x, y, z) - - .. testcode:: - - import anki_vector - with anki_vector.Robot() as robot: - current_accel = robot.accel - """ - return self._accel - - @property - @util.block_while_none() - def gyro(self) -> util.Vector3: - """The current gyroscope reading (x, y, z) - - .. testcode:: - - import anki_vector - with anki_vector.Robot() as robot: - current_gyro = robot.gyro - """ - return self._gyro - - @property - @util.block_while_none() - def carrying_object_id(self) -> int: - """The ID of the object currently being carried (-1 if none) - - .. testcode:: - - import anki_vector - from anki_vector.util import degrees - - # Set the robot so that he can see a cube. - with anki_vector.Robot() as robot: - robot.behavior.set_head_angle(degrees(0.0)) - robot.behavior.set_lift_height(0.0) - - robot.world.connect_cube() - - if robot.world.connected_light_cube: - robot.behavior.pickup_object(robot.world.connected_light_cube) - - print("carrying_object_id: ", robot.carrying_object_id) - """ - return self._carrying_object_id - - @property - @util.block_while_none() - def head_tracking_object_id(self) -> int: - """The ID of the object the head is tracking to (-1 if none) - - .. testcode:: - - import anki_vector - with anki_vector.Robot() as robot: - current_head_tracking_object_id = robot.head_tracking_object_id - """ - return self._head_tracking_object_id - - @property - @util.block_while_none() - def localized_to_object_id(self) -> int: - """The ID of the object that the robot is localized to (-1 if none) - - .. testcode:: - - import anki_vector - with anki_vector.Robot() as robot: - current_localized_to_object_id = robot.localized_to_object_id - """ - return self._localized_to_object_id - - # TODO Move to photos or somewhere else - @property - @util.block_while_none() - def last_image_time_stamp(self) -> int: - """The robot's timestamp for the last image seen. - - .. testcode:: - - import anki_vector - with anki_vector.Robot() as robot: - current_last_image_time_stamp = robot.last_image_time_stamp - """ - return self._last_image_time_stamp - - @property - def status(self) -> status.RobotStatus: - """A property that exposes various status properties of the robot. - - This status provides a simple mechanism to, for example, detect if any - of Vector's motors are moving, determine if Vector is being held, or if - he is on the charger. The full list is available in the - :class:`RobotStatus ` class documentation. - - .. testcode:: - - import anki_vector - with anki_vector.Robot() as robot: - if robot.status.is_being_held: - print("Vector is being held!") - else: - print("Vector is not being held.") - """ - return self._status - - @property - def enable_audio_feed(self) -> bool: - """The audio feed enabled/disabled - - :getter: Returns whether the audio feed is enabled - :setter: Enable/disable the audio feed - - .. code-block:: python - - import asyncio - import time - - import anki_vector - - with anki_vector.Robot(enable_audio_feed=True) as robot: - time.sleep(5) - robot.enable_audio_feed = False - time.sleep(5) - """ - # TODO When audio is ready, convert `.. code-block:: python` to `.. testcode::` - return self._enable_audio_feed - - @enable_audio_feed.setter - def enable_audio_feed(self, enable) -> None: - self._enable_audio_feed = enable - # TODO add audio feed enablement when ready - - # Unpack streamed data to robot's internal properties - def _unpack_robot_state(self, _robot, _event_type, msg): - self._pose = util.Pose(x=msg.pose.x, y=msg.pose.y, z=msg.pose.z, - q0=msg.pose.q0, q1=msg.pose.q1, - q2=msg.pose.q2, q3=msg.pose.q3, - origin_id=msg.pose.origin_id) - self._pose_angle_rad = msg.pose_angle_rad - self._pose_pitch_rad = msg.pose_pitch_rad - self._left_wheel_speed_mmps = msg.left_wheel_speed_mmps - self._right_wheel_speed_mmps = msg.right_wheel_speed_mmps - self._head_angle_rad = msg.head_angle_rad - self._lift_height_mm = msg.lift_height_mm - self._accel = util.Vector3(msg.accel.x, msg.accel.y, msg.accel.z) - self._gyro = util.Vector3(msg.gyro.x, msg.gyro.y, msg.gyro.z) - self._carrying_object_id = msg.carrying_object_id - self._head_tracking_object_id = msg.head_tracking_object_id - self._localized_to_object_id = msg.localized_to_object_id - self._last_image_time_stamp = msg.last_image_time_stamp - self._status.set(msg.status) - - def connect(self, timeout: int = 10) -> None: - """Start the connection to Vector. - - .. testcode:: - - import anki_vector - - robot = anki_vector.Robot() - robot.connect() - robot.anim.play_animation_trigger("GreetAfterLongTime") - robot.disconnect() - - :param timeout: The time to allow for a connection before a - :class:`anki_vector.exceptions.VectorTimeoutException` is raised. - """ - self.conn.connect(timeout=timeout) - self.events.start(self.conn) - - # Initialize components - self._anim = animation.AnimationComponent(self) - self._audio = audio.AudioComponent(self) - self._behavior = behavior.BehaviorComponent(self) - self._faces = faces.FaceComponent(self) - self._motors = motors.MotorComponent(self) - self._nav_map = nav_map.NavMapComponent(self) - self._screen = screen.ScreenComponent(self) - self._photos = photos.PhotographComponent(self) - self._proximity = proximity.ProximityComponent(self) - self._touch = touch.TouchComponent(self) - self._viewer = viewer.ViewerComponent(self) - self._viewer_3d = viewer.Viewer3DComponent(self) - self._vision = vision.VisionComponent(self) - self._world = world.World(self) - self._camera = camera.CameraComponent(self) - - if self.cache_animation_lists: - # Load animation triggers and animations so they are ready to play when requested - anim_request = self._anim.load_animation_list() - if isinstance(anim_request, concurrent.futures.Future): - anim_request.result() - anim_trigger_request = self._anim.load_animation_trigger_list() - if isinstance(anim_trigger_request, concurrent.futures.Future): - anim_trigger_request.result() - - # TODO enable audio feed when ready - - # Start rendering camera feed - if self._show_viewer: - self.camera.init_camera_feed() - self.viewer.show() - - if self._show_3d_viewer: - self.viewer_3d.show() - - if self._enable_nav_map_feed: - self.nav_map.init_nav_map_feed() - - # Enable face detection, to allow Vector to add faces to its world view - if self.conn.requires_behavior_control: - face_detection = self.vision.enable_face_detection(detect_faces=self.enable_face_detection, estimate_expression=self.estimate_facial_expression) - if isinstance(face_detection, concurrent.futures.Future): - face_detection.result() - object_detection = self.vision.enable_custom_object_detection(detect_custom_objects=self.enable_custom_object_detection) - if isinstance(object_detection, concurrent.futures.Future): - object_detection.result() - - # Subscribe to a callback that updates the robot's local properties - self.events.subscribe(self._unpack_robot_state, - events.Events.robot_state, - _on_connection_thread=True) - - # get the camera configuration from the robot - response = self._camera.get_camera_config() - if isinstance(response, concurrent.futures.Future): - response = response.result() - self._camera.set_config(response) - - # Subscribe to a callback for camera exposure settings - self.events.subscribe(self._camera.update_state, - events.Events.camera_settings_update, - _on_connection_thread=True) - - # access the pose to prove it has gotten back from the event stream once - try: - if not self.pose: - pass - except VectorPropertyValueNotReadyException as e: - raise VectorUnreliableEventStreamException() from e - - def disconnect(self) -> None: - """Close the connection with Vector. - - .. testcode:: - - import anki_vector - robot = anki_vector.Robot() - robot.connect() - robot.anim.play_animation_trigger("GreetAfterLongTime") - robot.disconnect() - """ - if self.conn.requires_behavior_control: - self.vision.close() - - # Stop rendering video - self.viewer.close() - - # Stop rendering 3d video - self.viewer_3d.close() - - # Shutdown camera feed - self.camera.close_camera_feed() - - # TODO shutdown audio feed when available - - # Shutdown nav map feed - self.nav_map.close_nav_map_feed() - - # Close the world and cleanup its objects - self.world.close() - - self.proximity.close() - self.touch.close() - - self.events.close() - self.conn.close() - - def __enter__(self): - self.connect(self.behavior_activation_timeout) - return self - - def __exit__(self, exc_type, exc_val, exc_tb): - self.disconnect() - - @on_connection_thread(requires_control=False) - async def get_battery_state(self) -> protocol.BatteryStateResponse: - """Check the current state of the robot and cube batteries. - - The robot is considered fully-charged above 4.1 volts. At 3.6V, the robot is approaching low charge. - - Robot battery level values are as follows: - - +-------+---------+---------------------------------------------------------------+ - | Value | Level | Description | - +=======+=========+===============================================================+ - | 1 | Low | 3.6V or less. If on charger, 4V or less. | - +-------+---------+---------------------------------------------------------------+ - | 2 | Nominal | Normal operating levels. | - +-------+---------+---------------------------------------------------------------+ - | 3 | Full | This state can only be achieved when Vector is on the charger | - +-------+---------+---------------------------------------------------------------+ - - Cube battery level values are shown below: - - +-------+---------+---------------------------------------------------------------+ - | Value | Level | Description | - +=======+=========+===============================================================+ - | 1 | Low | 1.1V or less. | - +-------+---------+---------------------------------------------------------------+ - | 2 | Normal | Normal operating levels. | - +-------+---------+---------------------------------------------------------------+ - - .. testcode:: - - import anki_vector - - with anki_vector.Robot() as robot: - print("Connecting to a cube...") - robot.world.connect_cube() - - battery_state = robot.get_battery_state() - if battery_state: - print("Robot battery voltage: {0}".format(battery_state.battery_volts)) - print("Robot battery Level: {0}".format(battery_state.battery_level)) - print("Robot battery is charging: {0}".format(battery_state.is_charging)) - print("Robot is on charger platform: {0}".format(battery_state.is_on_charger_platform)) - print("Robot suggested charger time: {0}".format(battery_state.suggested_charger_sec)) - print("Cube battery level: {0}".format(battery_state.cube_battery.level)) - print("Cube battery voltage: {0}".format(battery_state.cube_battery.battery_volts)) - print("Cube battery seconds since last reading: {0}".format(battery_state.cube_battery.time_since_last_reading_sec)) - print("Cube battery factory id: {0}".format(battery_state.cube_battery.factory_id)) - """ - get_battery_state_request = protocol.BatteryStateRequest() - return await self.conn.grpc_interface.BatteryState(get_battery_state_request) - - @on_connection_thread(requires_control=False) - async def get_version_state(self) -> protocol.VersionStateResponse: - """Get the versioning information for Vector, including Vector's os_version and engine_build_id. - - .. testcode:: - - import anki_vector - with anki_vector.Robot() as robot: - version_state = robot.get_version_state() - if version_state: - print("Robot os_version: {0}".format(version_state.os_version)) - print("Robot engine_build_id: {0}".format(version_state.engine_build_id)) - """ - get_version_state_request = protocol.VersionStateRequest() - return await self.conn.grpc_interface.VersionState(get_version_state_request) - - -class AsyncRobot(Robot): - """The AsyncRobot object is just like the Robot object, but allows multiple commands - to be executed at the same time. To achieve this, all grpc function calls also - return a :class:`concurrent.futures.Future`. - - 1. Using :code:`with`: it works just like opening a file, and will close when - the :code:`with` block's indentation ends. - - .. testcode:: - - import anki_vector - from anki_vector.util import degrees - - # Create the robot connection - with anki_vector.AsyncRobot() as robot: - # Start saying text asynchronously - say_future = robot.behavior.say_text("Now is the time") - # Turn robot, wait for completion - turn_future = robot.behavior.turn_in_place(degrees(3*360)) - turn_future.result() - # Play greet animation trigger, wait for completion - greet_future = robot.anim.play_animation_trigger("GreetAfterLongTime") - greet_future.result() - # Make sure text has been spoken - say_future.result() - - 2. Using :func:`connect` and :func:`disconnect` to explicitly open and close the connection: - it allows the robot's connection to continue in the context in which it started. - - .. testcode:: - - import anki_vector - from anki_vector.util import degrees - - # Create a Robot object - robot = anki_vector.AsyncRobot() - # Connect to Vector - robot.connect() - # Start saying text asynchronously - say_future = robot.behavior.say_text("Now is the time") - # Turn robot, wait for completion - turn_future = robot.behavior.turn_in_place(degrees(3 * 360)) - turn_future.result() - # Play greet animation trigger, wait for completion - greet_future = robot.anim.play_animation_trigger("GreetAfterLongTime") - greet_future.result() - # Make sure text has been spoken - say_future.result() - # Disconnect from Vector - robot.disconnect() - - When getting callbacks from the event stream, it's important to understand that function calls - return a :class:`concurrent.futures.Future` and not an :class:`asyncio.Future`. This means any - async callback functions will need to use :func:`asyncio.wrap_future` to be able to await the - function's response. - - .. testcode:: - - import asyncio - import time - - import anki_vector - - async def callback(robot, event_type, event): - await asyncio.wrap_future(robot.anim.play_animation_trigger('GreetAfterLongTime')) - await asyncio.wrap_future(robot.behavior.set_head_angle(anki_vector.util.degrees(40))) - - if __name__ == "__main__": - args = anki_vector.util.parse_command_args() - with anki_vector.AsyncRobot(serial=args.serial, enable_face_detection=True) as robot: - robot.behavior.set_head_angle(anki_vector.util.degrees(40)) - robot.events.subscribe(callback, anki_vector.events.Events.robot_observed_face) - - # Waits 10 seconds. Show Vector your face. - time.sleep(10) - - :param serial: Vector's serial number. The robot's serial number (ex. 00e20100) is located on the underside of Vector, - or accessible from Vector's debug screen. Used to identify which Vector configuration to load. - :param ip: Vector's IP Address. (optional) - :param config: A custom :class:`dict` to override values in Vector's configuration. (optional) - Example: :code:`{"cert": "/path/to/file.cert", "name": "Vector-XXXX", "guid": ""}` - where :code:`cert` is the certificate to identify Vector, :code:`name` is the name on Vector's face - when his backpack is double-clicked on the charger, and :code:`guid` is the authorization token - that identifies the SDK user. Note: Never share your authentication credentials with anyone. - :param default_logging: Toggle default logging. - :param behavior_activation_timeout: The time to wait for control of the robot before failing. - :param cache_animation_lists: Get the list of animation triggers and animations available at startup. - :param enable_face_detection: Turn on face detection. - :param estimate_facial_expression: Turn estimating facial expression on/off. - :param enable_audio_feed: Turn audio feed on/off. - :param enable_custom_object_detection: Turn custom object detection on/off. - :param enable_nav_map_feed: Turn navigation map feed on/off. - :param show_viewer: Specifies whether to display a view of Vector's camera in a window. - :param show_3d_viewer: Specifies whether to display a 3D view of Vector's understanding of the world in a window. - :param behavior_control_level: Request control of Vector's behavior system at a specific level of control. Pass - :code:`None` if behavior control is not needed. - See :class:`ControlPriorityLevel` for more information.""" - - @functools.wraps(Robot.__init__) - def __init__(self, *args, **kwargs): - super(AsyncRobot, self).__init__(*args, **kwargs) - self._force_async = True +# Copyright (c) 2018 Anki, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License in the file LICENSE.txt or at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +This contains the :class:`Robot` and :class:`AsyncRobot` classes for managing Vector. + +:class:`Robot` will run all behaviors in sequence and directly return the results. + +:class:`AsyncRobot` will instead provide a :class:`concurrent.futures.Future` which the +caller may use to obtain the result when they desire. +""" + +# __all__ should order by constants, event classes, other classes, functions. +__all__ = ['Robot', 'AsyncRobot'] + +import concurrent +import functools + +from . import (animation, audio, behavior, camera, + events, faces, motors, nav_map, screen, + photos, proximity, status, touch, + util, viewer, vision, world) +from .connection import (Connection, + on_connection_thread, + ControlPriorityLevel) +from .exceptions import (VectorNotReadyException, + VectorPropertyValueNotReadyException, + VectorUnreliableEventStreamException) +from .viewer import (ViewerComponent, Viewer3DComponent) +from .messaging import protocol +from .mdns import VectorMdns + + +class Robot: + """The Robot object is responsible for managing the state and connections + to a Vector, and is typically the entry-point to running the sdk. + + The majority of the robot will not work until it is properly connected + to Vector. There are two ways to get connected: + + 1. Using :code:`with`: it works just like opening a file, and will close when + the :code:`with` block's indentation ends. + + + .. testcode:: + + import anki_vector + + # Create the robot connection + with anki_vector.Robot() as robot: + # Run your commands + robot.anim.play_animation_trigger("GreetAfterLongTime") + + 2. Using :func:`connect` and :func:`disconnect` to explicitly open and close the connection: + it allows the robot's connection to continue in the context in which it started. + + .. testcode:: + + import anki_vector + + # Create a Robot object + robot = anki_vector.Robot() + # Connect to the Robot + robot.connect() + # Run your commands + robot.anim.play_animation_trigger("GreetAfterLongTime") + # Disconnect from Vector + robot.disconnect() + + :param serial: Vector's serial number. The robot's serial number (ex. 00e20100) is located on the underside of Vector, + or accessible from Vector's debug screen. Used to identify which Vector configuration to load. + :param ip: Vector's IP address. (optional) + :param name: Vector's name (in format :code:`"Vector-XXXX"`) to be used for mDNS discovery. If a Vector with the given name + is discovered, the :code:`ip` parameter (and config field) will be overridden. + :param config: A custom :class:`dict` to override values in Vector's configuration. (optional) + Example: :code:`{"cert": "/path/to/file.cert", "name": "Vector-XXXX", "guid": ""}` + where :code:`cert` is the certificate to identify Vector, :code:`name` is the name on Vector's face + when his backpack is double-clicked on the charger, and :code:`guid` is the authorization token + that identifies the SDK user. Note: Never share your authentication credentials with anyone. + :param default_logging: Toggle default logging. + :param behavior_activation_timeout: The time to wait for control of the robot before failing. + :param cache_animation_lists: Get the list of animation triggers and animations available at startup. + :param enable_face_detection: Turn on face detection. + :param estimate_facial_expression: Turn estimating facial expression on/off. Enabling :code:`estimate_facial_expression` + returns a facial expression, the expression values and the :class:`anki_vector.util.ImageRect` + for observed face regions (eyes, nose, and mouth) as part of the :code:`RobotObservedFace` event. + It is turned off by default as the number of :code:`RobotObservedFace` events + are reduced due to the increased processing time. + :param enable_audio_feed: Turn audio feed on/off. + :param enable_custom_object_detection: Turn custom object detection on/off. + :param enable_nav_map_feed: Turn navigation map feed on/off. + :param show_viewer: Specifies whether to display a view of Vector's camera in a window. + :param show_3d_viewer: Specifies whether to display a 3D view of Vector's understanding of the world in a window. + :param behavior_control_level: Request control of Vector's behavior system at a specific level of control. Pass + :code:`None` if behavior control is not needed. + See :class:`ControlPriorityLevel` for more information.""" + + def __init__(self, + serial: str = None, + ip: str = None, + name: str = None, + config: dict = None, + escape_pod: bool = False, + default_logging: bool = True, + behavior_activation_timeout: int = 10, + cache_animation_lists: bool = True, + enable_face_detection: bool = False, + estimate_facial_expression: bool = False, + enable_audio_feed: bool = False, + enable_custom_object_detection: bool = False, + enable_nav_map_feed: bool = None, + show_viewer: bool = False, + show_3d_viewer: bool = False, + behavior_control_level: ControlPriorityLevel = ControlPriorityLevel.DEFAULT_PRIORITY): + if default_logging: + util.setup_basic_logging() + self.logger = util.get_class_logger(__name__, self) + self._force_async = False + config = config if config is not None else {} + config = {**util.read_configuration(serial, name, self.logger, escape_pod), **config} + + if name is not None: + vector_mdns = VectorMdns.find_vector(name) + + if vector_mdns is not None: + ip = vector_mdns['ipv4'] + + self._escape_pod = escape_pod + self._name = config["name"] if 'name' in config else None + self._ip = ip if ip is not {} else config["ip"] if 'ip' in config else None + self._cert_file = config["cert"] if 'cert' in config else None + self._guid = config["guid"] if 'guid' in config else None + self._port = config["port"] if 'port' in config else "443" + + if (not escape_pod) and (self._name is None or self._ip is None or self._cert_file is None or self._guid is None): + raise ValueError("The Robot object requires a serial and for Vector to be logged in (using the app then running the `python3 -m anki_vector.configure`).\n" + "You may also provide the values necessary for connection through the config parameter. ex: " + '{"name":"Vector-XXXX", "ip":"XX.XX.XX.XX", "cert":"/path/to/cert_file", "guid":""}') + + if (escape_pod) and (self._ip is None): + raise ValueError('Could not find the sdk configuration file. Please run `python3 -m anki_vector.configure_pod` to set up your Vector for SDK usage.') + + #: :class:`anki_vector.connection.Connection`: The active connection to the robot. + self._conn = Connection(self._name, ':'.join([self._ip, self._port]), self._cert_file, self._guid, self._escape_pod, behavior_control_level=behavior_control_level) + self._events = events.EventHandler(self) + + # placeholders for components before they exist + self._anim: animation.AnimationComponent = None + self._audio: audio.AudioComponent = None + self._behavior: behavior.BehaviorComponent = None + self._camera: camera.CameraComponent = None + self._faces: faces.FaceComponent = None + self._motors: motors.MotorComponent = None + self._nav_map: nav_map.NavMapComponent = None + self._screen: screen.ScreenComponent = None + self._photos: photos.PhotographComponent = None + self._proximity: proximity.ProximityComponent = None + self._touch: touch.TouchComponent = None + self._viewer: viewer.ViewerComponent = None + self._viewer_3d: viewer.Viewer3DComponent = None + self._vision: vision.VisionComponent = None + self._world: world.World = None + + self.behavior_activation_timeout = behavior_activation_timeout + self.enable_face_detection = enable_face_detection + self.estimate_facial_expression = estimate_facial_expression + self.enable_custom_object_detection = enable_custom_object_detection + self.cache_animation_lists = cache_animation_lists + + # Robot state/sensor data + self._pose: util.Pose = None + self._pose_angle_rad: float = None + self._pose_pitch_rad: float = None + self._left_wheel_speed_mmps: float = None + self._right_wheel_speed_mmps: float = None + self._head_angle_rad: float = None + self._lift_height_mm: float = None + self._accel: util.Vector3 = None + self._gyro: util.Vector3 = None + self._carrying_object_id: float = None + self._head_tracking_object_id: float = None + self._localized_to_object_id: float = None + self._last_image_time_stamp: float = None + self._status: status.RobotStatus = status.RobotStatus() + + self._enable_audio_feed = enable_audio_feed + if enable_nav_map_feed is not None: + self._enable_nav_map_feed = enable_nav_map_feed + else: + self._enable_nav_map_feed = False + self._show_viewer = show_viewer + self._show_3d_viewer = show_3d_viewer + if show_3d_viewer and enable_nav_map_feed is None: + self.logger.warning("enable_nav_map_feed should be True for 3d viewer to render correctly.") + self._enable_nav_map_feed = True + + @property + def force_async(self) -> bool: + """A flag used to determine if this is a :class:`Robot` or :class:`AsyncRobot`.""" + return self._force_async + + @property + def conn(self) -> Connection: + """A reference to the :class:`~anki_vector.connection.Connection` instance.""" + return self._conn + + @property + def events(self) -> events.EventHandler: + """A reference to the :class:`~anki_vector.events.EventHandler` instance.""" + return self._events + + @property + def anim(self) -> animation.AnimationComponent: + """A reference to the :class:`~anki_vector.animation.AnimationComponent` instance.""" + if self._anim is None: + raise VectorNotReadyException("AnimationComponent is not yet initialized") + return self._anim + + @property + def audio(self) -> audio.AudioComponent: + """The audio instance used to control Vector's microphone feed and speaker playback.""" + + if self._audio is None: + raise VectorNotReadyException("AudioComponent is not yet initialized") + return self._audio + + @property + def behavior(self) -> behavior.BehaviorComponent: + """A reference to the :class:`~anki_vector.behavior.BehaviorComponent` instance.""" + return self._behavior + + @property + def camera(self) -> camera.CameraComponent: + """The :class:`~anki_vector.camera.CameraComponent` instance used to control Vector's camera feed. + + .. testcode:: + + import anki_vector + + with anki_vector.Robot() as robot: + robot.camera.init_camera_feed() + image = robot.camera.latest_image + image.raw_image.show() + """ + if self._camera is None: + raise VectorNotReadyException("CameraComponent is not yet initialized") + return self._camera + + @property + def faces(self) -> faces.FaceComponent: + """A reference to the :class:`~anki_vector.faces.FaceComponent` instance.""" + if self._faces is None: + raise VectorNotReadyException("FaceComponent is not yet initialized") + return self._faces + + @property + def motors(self) -> motors.MotorComponent: + """A reference to the :class:`~anki_vector.motors.MotorComponent` instance.""" + if self._motors is None: + raise VectorNotReadyException("MotorComponent is not yet initialized") + return self._motors + + @property + def nav_map(self) -> nav_map.NavMapComponent: + """A reference to the :class:`~anki_vector.nav_map.NavMapComponent` instance.""" + if self._nav_map is None: + raise VectorNotReadyException("NavMapComponent is not yet initialized") + return self._nav_map + + @property + def screen(self) -> screen.ScreenComponent: + """A reference to the :class:`~anki_vector.screen.ScreenComponent` instance.""" + if self._screen is None: + raise VectorNotReadyException("ScreenComponent is not yet initialized") + return self._screen + + @property + def photos(self) -> photos.PhotographComponent: + """A reference to the :class:`~anki_vector.photos.PhotographComponent` instance.""" + if self._photos is None: + raise VectorNotReadyException("PhotographyComponent is not yet initialized") + return self._photos + + @property + def proximity(self) -> proximity.ProximityComponent: + """:class:`~anki_vector.proximity.ProximityComponent` containing state related to object proximity detection. + + .. code-block:: python + + import anki_vector + with anki_vector.Robot() as robot: + proximity_data = robot.proximity.last_sensor_reading + if proximity_data is not None: + print(proximity_data.distance) + """ + return self._proximity + + @property + def touch(self) -> touch.TouchComponent: + """:class:`~anki_vector.touch.TouchComponent` containing state related to object touch detection. + + .. testcode:: + + import anki_vector + with anki_vector.Robot() as robot: + print('Robot is being touched: {0}'.format(robot.touch.last_sensor_reading.is_being_touched)) + """ + return self._touch + + @property + def viewer(self) -> ViewerComponent: + """The :class:`~anki_vector.viewer.ViewerComponent` instance used to render Vector's camera feed. + + .. testcode:: + + import time + + import anki_vector + + with anki_vector.Robot() as robot: + # Render video for 5 seconds + robot.viewer.show() + time.sleep(5) + + # Disable video render and camera feed for 5 seconds + robot.viewer.close() + """ + if self._viewer is None: + raise VectorNotReadyException("ViewerComponent is not yet initialized") + return self._viewer + + @property + def viewer_3d(self) -> Viewer3DComponent: + """The :class:`~anki_vector.viewer.Viewer3DComponent` instance used to render Vector's navigation map. + + .. testcode:: + + import time + + import anki_vector + + with anki_vector.Robot(show_3d_viewer=True, enable_nav_map_feed=True) as robot: + # Render 3D view of navigation map for 5 seconds + time.sleep(5) + """ + if self._viewer_3d is None: + raise VectorNotReadyException("Viewer3DComponent is not yet initialized") + return self._viewer_3d + + @property + def vision(self) -> vision.VisionComponent: + """:class:`~anki_vector.vision.VisionComponent` containing functionality related to vision based object detection. + + .. testcode:: + + import anki_vector + with anki_vector.Robot() as robot: + robot.vision.enable_custom_object_detection() + """ + return self._vision + + @property + def world(self) -> world.World: + """A reference to the :class:`~anki_vector.world.World` instance, or None if the World is not yet initialized.""" + if self._world is None: + raise VectorNotReadyException("WorldComponent is not yet initialized") + return self._world + + @property + @util.block_while_none() + def pose(self) -> util.Pose: + """:class:`anki_vector.util.Pose`: The current pose (position and orientation) of Vector. + + .. testcode:: + + import anki_vector + with anki_vector.Robot() as robot: + current_robot_pose = robot.pose + """ + return self._pose + + @property + @util.block_while_none() + def pose_angle_rad(self) -> float: + """Vector's pose angle (heading in X-Y plane). + + .. testcode:: + + import anki_vector + with anki_vector.Robot() as robot: + current_pose_angle_rad = robot.pose_angle_rad + """ + return self._pose_angle_rad + + @property + @util.block_while_none() + def pose_pitch_rad(self) -> float: + """Vector's pose pitch (angle up/down). + + .. testcode:: + + import anki_vector + with anki_vector.Robot() as robot: + current_pose_pitch_rad = robot.pose_pitch_rad + """ + return self._pose_pitch_rad + + @property + @util.block_while_none() + def left_wheel_speed_mmps(self) -> float: + """Vector's left wheel speed in mm/sec + + .. testcode:: + + import anki_vector + with anki_vector.Robot() as robot: + current_left_wheel_speed_mmps = robot.left_wheel_speed_mmps + """ + return self._left_wheel_speed_mmps + + @property + @util.block_while_none() + def right_wheel_speed_mmps(self) -> float: + """Vector's right wheel speed in mm/sec + + .. testcode:: + + import anki_vector + with anki_vector.Robot() as robot: + current_right_wheel_speed_mmps = robot.right_wheel_speed_mmps + """ + return self._right_wheel_speed_mmps + + @property + @util.block_while_none() + def head_angle_rad(self) -> float: + """Vector's head angle (up/down). + + .. testcode:: + + import anki_vector + with anki_vector.Robot() as robot: + current_head_angle_rad = robot.head_angle_rad + """ + return self._head_angle_rad + + @property + @util.block_while_none() + def lift_height_mm(self) -> float: + """Height of Vector's lift from the ground. + + .. testcode:: + + import anki_vector + with anki_vector.Robot() as robot: + current_lift_height_mm = robot.lift_height_mm + """ + return self._lift_height_mm + + @property + @util.block_while_none() + def accel(self) -> util.Vector3: + """:class:`anki_vector.util.Vector3`: The current accelerometer reading (x, y, z) + + .. testcode:: + + import anki_vector + with anki_vector.Robot() as robot: + current_accel = robot.accel + """ + return self._accel + + @property + @util.block_while_none() + def gyro(self) -> util.Vector3: + """The current gyroscope reading (x, y, z) + + .. testcode:: + + import anki_vector + with anki_vector.Robot() as robot: + current_gyro = robot.gyro + """ + return self._gyro + + @property + @util.block_while_none() + def carrying_object_id(self) -> int: + """The ID of the object currently being carried (-1 if none) + + .. testcode:: + + import anki_vector + from anki_vector.util import degrees + + # Set the robot so that he can see a cube. + with anki_vector.Robot() as robot: + robot.behavior.set_head_angle(degrees(0.0)) + robot.behavior.set_lift_height(0.0) + + robot.world.connect_cube() + + if robot.world.connected_light_cube: + robot.behavior.pickup_object(robot.world.connected_light_cube) + + print("carrying_object_id: ", robot.carrying_object_id) + """ + return self._carrying_object_id + + @property + @util.block_while_none() + def head_tracking_object_id(self) -> int: + """The ID of the object the head is tracking to (-1 if none) + + .. testcode:: + + import anki_vector + with anki_vector.Robot() as robot: + current_head_tracking_object_id = robot.head_tracking_object_id + """ + return self._head_tracking_object_id + + @property + @util.block_while_none() + def localized_to_object_id(self) -> int: + """The ID of the object that the robot is localized to (-1 if none) + + .. testcode:: + + import anki_vector + with anki_vector.Robot() as robot: + current_localized_to_object_id = robot.localized_to_object_id + """ + return self._localized_to_object_id + + # TODO Move to photos or somewhere else + @property + @util.block_while_none() + def last_image_time_stamp(self) -> int: + """The robot's timestamp for the last image seen. + + .. testcode:: + + import anki_vector + with anki_vector.Robot() as robot: + current_last_image_time_stamp = robot.last_image_time_stamp + """ + return self._last_image_time_stamp + + @property + def status(self) -> status.RobotStatus: + """A property that exposes various status properties of the robot. + + This status provides a simple mechanism to, for example, detect if any + of Vector's motors are moving, determine if Vector is being held, or if + he is on the charger. The full list is available in the + :class:`RobotStatus ` class documentation. + + .. testcode:: + + import anki_vector + with anki_vector.Robot() as robot: + if robot.status.is_being_held: + print("Vector is being held!") + else: + print("Vector is not being held.") + """ + return self._status + + @property + def enable_audio_feed(self) -> bool: + """The audio feed enabled/disabled + + :getter: Returns whether the audio feed is enabled + :setter: Enable/disable the audio feed + + .. code-block:: python + + import asyncio + import time + + import anki_vector + + with anki_vector.Robot(enable_audio_feed=True) as robot: + time.sleep(5) + robot.enable_audio_feed = False + time.sleep(5) + """ + # TODO When audio is ready, convert `.. code-block:: python` to `.. testcode::` + return self._enable_audio_feed + + @enable_audio_feed.setter + def enable_audio_feed(self, enable) -> None: + self._enable_audio_feed = enable + # TODO add audio feed enablement when ready + + # Unpack streamed data to robot's internal properties + def _unpack_robot_state(self, _robot, _event_type, msg): + self._pose = util.Pose(x=msg.pose.x, y=msg.pose.y, z=msg.pose.z, + q0=msg.pose.q0, q1=msg.pose.q1, + q2=msg.pose.q2, q3=msg.pose.q3, + origin_id=msg.pose.origin_id) + self._pose_angle_rad = msg.pose_angle_rad + self._pose_pitch_rad = msg.pose_pitch_rad + self._left_wheel_speed_mmps = msg.left_wheel_speed_mmps + self._right_wheel_speed_mmps = msg.right_wheel_speed_mmps + self._head_angle_rad = msg.head_angle_rad + self._lift_height_mm = msg.lift_height_mm + self._accel = util.Vector3(msg.accel.x, msg.accel.y, msg.accel.z) + self._gyro = util.Vector3(msg.gyro.x, msg.gyro.y, msg.gyro.z) + self._carrying_object_id = msg.carrying_object_id + self._head_tracking_object_id = msg.head_tracking_object_id + self._localized_to_object_id = msg.localized_to_object_id + self._last_image_time_stamp = msg.last_image_time_stamp + self._status.set(msg.status) + + def connect(self, timeout: int = 10) -> None: + """Start the connection to Vector. + + .. testcode:: + + import anki_vector + + robot = anki_vector.Robot() + robot.connect() + robot.anim.play_animation_trigger("GreetAfterLongTime") + robot.disconnect() + + :param timeout: The time to allow for a connection before a + :class:`anki_vector.exceptions.VectorTimeoutException` is raised. + """ + self.conn.connect(timeout=timeout) + self.events.start(self.conn) + + # Initialize components + self._anim = animation.AnimationComponent(self) + self._audio = audio.AudioComponent(self) + self._behavior = behavior.BehaviorComponent(self) + self._faces = faces.FaceComponent(self) + self._motors = motors.MotorComponent(self) + self._nav_map = nav_map.NavMapComponent(self) + self._screen = screen.ScreenComponent(self) + self._photos = photos.PhotographComponent(self) + self._proximity = proximity.ProximityComponent(self) + self._touch = touch.TouchComponent(self) + self._viewer = viewer.ViewerComponent(self) + self._viewer_3d = viewer.Viewer3DComponent(self) + self._vision = vision.VisionComponent(self) + self._world = world.World(self) + self._camera = camera.CameraComponent(self) + + if self.cache_animation_lists: + # Load animation triggers and animations so they are ready to play when requested + anim_request = self._anim.load_animation_list() + if isinstance(anim_request, concurrent.futures.Future): + anim_request.result() + anim_trigger_request = self._anim.load_animation_trigger_list() + if isinstance(anim_trigger_request, concurrent.futures.Future): + anim_trigger_request.result() + + # TODO enable audio feed when ready + + # Start rendering camera feed + if self._show_viewer: + self.camera.init_camera_feed() + self.viewer.show() + + if self._show_3d_viewer: + self.viewer_3d.show() + + if self._enable_nav_map_feed: + self.nav_map.init_nav_map_feed() + + # Enable face detection, to allow Vector to add faces to its world view + if self.conn.requires_behavior_control: + face_detection = self.vision.enable_face_detection(detect_faces=self.enable_face_detection, estimate_expression=self.estimate_facial_expression) + if isinstance(face_detection, concurrent.futures.Future): + face_detection.result() + object_detection = self.vision.enable_custom_object_detection(detect_custom_objects=self.enable_custom_object_detection) + if isinstance(object_detection, concurrent.futures.Future): + object_detection.result() + + # Subscribe to a callback that updates the robot's local properties + self.events.subscribe(self._unpack_robot_state, + events.Events.robot_state, + _on_connection_thread=True) + + # get the camera configuration from the robot + response = self._camera.get_camera_config() + if isinstance(response, concurrent.futures.Future): + response = response.result() + self._camera.set_config(response) + + # Subscribe to a callback for camera exposure settings + self.events.subscribe(self._camera.update_state, + events.Events.camera_settings_update, + _on_connection_thread=True) + + # access the pose to prove it has gotten back from the event stream once + try: + if not self.pose: + pass + except VectorPropertyValueNotReadyException as e: + raise VectorUnreliableEventStreamException() from e + + def disconnect(self) -> None: + """Close the connection with Vector. + + .. testcode:: + + import anki_vector + robot = anki_vector.Robot() + robot.connect() + robot.anim.play_animation_trigger("GreetAfterLongTime") + robot.disconnect() + """ + if self.conn.requires_behavior_control: + self.vision.close() + + # Stop rendering video + self.viewer.close() + + # Stop rendering 3d video + self.viewer_3d.close() + + # Shutdown camera feed + self.camera.close_camera_feed() + + # TODO shutdown audio feed when available + + # Shutdown nav map feed + self.nav_map.close_nav_map_feed() + + # Close the world and cleanup its objects + self.world.close() + + self.proximity.close() + self.touch.close() + + self.events.close() + self.conn.close() + + def __enter__(self): + self.connect(self.behavior_activation_timeout) + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.disconnect() + + @on_connection_thread(requires_control=False) + async def get_battery_state(self) -> protocol.BatteryStateResponse: + """Check the current state of the robot and cube batteries. + + The robot is considered fully-charged above 4.1 volts. At 3.6V, the robot is approaching low charge. + + Robot battery level values are as follows: + + +-------+---------+---------------------------------------------------------------+ + | Value | Level | Description | + +=======+=========+===============================================================+ + | 1 | Low | 3.6V or less. If on charger, 4V or less. | + +-------+---------+---------------------------------------------------------------+ + | 2 | Nominal | Normal operating levels. | + +-------+---------+---------------------------------------------------------------+ + | 3 | Full | This state can only be achieved when Vector is on the charger | + +-------+---------+---------------------------------------------------------------+ + + Cube battery level values are shown below: + + +-------+---------+---------------------------------------------------------------+ + | Value | Level | Description | + +=======+=========+===============================================================+ + | 1 | Low | 1.1V or less. | + +-------+---------+---------------------------------------------------------------+ + | 2 | Normal | Normal operating levels. | + +-------+---------+---------------------------------------------------------------+ + + .. testcode:: + + import anki_vector + + with anki_vector.Robot() as robot: + print("Connecting to a cube...") + robot.world.connect_cube() + + battery_state = robot.get_battery_state() + if battery_state: + print("Robot battery voltage: {0}".format(battery_state.battery_volts)) + print("Robot battery Level: {0}".format(battery_state.battery_level)) + print("Robot battery is charging: {0}".format(battery_state.is_charging)) + print("Robot is on charger platform: {0}".format(battery_state.is_on_charger_platform)) + print("Robot suggested charger time: {0}".format(battery_state.suggested_charger_sec)) + print("Cube battery level: {0}".format(battery_state.cube_battery.level)) + print("Cube battery voltage: {0}".format(battery_state.cube_battery.battery_volts)) + print("Cube battery seconds since last reading: {0}".format(battery_state.cube_battery.time_since_last_reading_sec)) + print("Cube battery factory id: {0}".format(battery_state.cube_battery.factory_id)) + """ + get_battery_state_request = protocol.BatteryStateRequest() + return await self.conn.grpc_interface.BatteryState(get_battery_state_request) + + @on_connection_thread(requires_control=False) + async def get_version_state(self) -> protocol.VersionStateResponse: + """Get the versioning information for Vector, including Vector's os_version and engine_build_id. + + .. testcode:: + + import anki_vector + with anki_vector.Robot() as robot: + version_state = robot.get_version_state() + if version_state: + print("Robot os_version: {0}".format(version_state.os_version)) + print("Robot engine_build_id: {0}".format(version_state.engine_build_id)) + """ + get_version_state_request = protocol.VersionStateRequest() + return await self.conn.grpc_interface.VersionState(get_version_state_request) + + +class AsyncRobot(Robot): + """The AsyncRobot object is just like the Robot object, but allows multiple commands + to be executed at the same time. To achieve this, all grpc function calls also + return a :class:`concurrent.futures.Future`. + + 1. Using :code:`with`: it works just like opening a file, and will close when + the :code:`with` block's indentation ends. + + .. testcode:: + + import anki_vector + from anki_vector.util import degrees + + # Create the robot connection + with anki_vector.AsyncRobot() as robot: + # Start saying text asynchronously + say_future = robot.behavior.say_text("Now is the time") + # Turn robot, wait for completion + turn_future = robot.behavior.turn_in_place(degrees(3*360)) + turn_future.result() + # Play greet animation trigger, wait for completion + greet_future = robot.anim.play_animation_trigger("GreetAfterLongTime") + greet_future.result() + # Make sure text has been spoken + say_future.result() + + 2. Using :func:`connect` and :func:`disconnect` to explicitly open and close the connection: + it allows the robot's connection to continue in the context in which it started. + + .. testcode:: + + import anki_vector + from anki_vector.util import degrees + + # Create a Robot object + robot = anki_vector.AsyncRobot() + # Connect to Vector + robot.connect() + # Start saying text asynchronously + say_future = robot.behavior.say_text("Now is the time") + # Turn robot, wait for completion + turn_future = robot.behavior.turn_in_place(degrees(3 * 360)) + turn_future.result() + # Play greet animation trigger, wait for completion + greet_future = robot.anim.play_animation_trigger("GreetAfterLongTime") + greet_future.result() + # Make sure text has been spoken + say_future.result() + # Disconnect from Vector + robot.disconnect() + + When getting callbacks from the event stream, it's important to understand that function calls + return a :class:`concurrent.futures.Future` and not an :class:`asyncio.Future`. This means any + async callback functions will need to use :func:`asyncio.wrap_future` to be able to await the + function's response. + + .. testcode:: + + import asyncio + import time + + import anki_vector + + async def callback(robot, event_type, event): + await asyncio.wrap_future(robot.anim.play_animation_trigger('GreetAfterLongTime')) + await asyncio.wrap_future(robot.behavior.set_head_angle(anki_vector.util.degrees(40))) + + if __name__ == "__main__": + args = anki_vector.util.parse_command_args() + with anki_vector.AsyncRobot(serial=args.serial, enable_face_detection=True) as robot: + robot.behavior.set_head_angle(anki_vector.util.degrees(40)) + robot.events.subscribe(callback, anki_vector.events.Events.robot_observed_face) + + # Waits 10 seconds. Show Vector your face. + time.sleep(10) + + :param serial: Vector's serial number. The robot's serial number (ex. 00e20100) is located on the underside of Vector, + or accessible from Vector's debug screen. Used to identify which Vector configuration to load. + :param ip: Vector's IP Address. (optional) + :param config: A custom :class:`dict` to override values in Vector's configuration. (optional) + Example: :code:`{"cert": "/path/to/file.cert", "name": "Vector-XXXX", "guid": ""}` + where :code:`cert` is the certificate to identify Vector, :code:`name` is the name on Vector's face + when his backpack is double-clicked on the charger, and :code:`guid` is the authorization token + that identifies the SDK user. Note: Never share your authentication credentials with anyone. + :param default_logging: Toggle default logging. + :param behavior_activation_timeout: The time to wait for control of the robot before failing. + :param cache_animation_lists: Get the list of animation triggers and animations available at startup. + :param enable_face_detection: Turn on face detection. + :param estimate_facial_expression: Turn estimating facial expression on/off. + :param enable_audio_feed: Turn audio feed on/off. + :param enable_custom_object_detection: Turn custom object detection on/off. + :param enable_nav_map_feed: Turn navigation map feed on/off. + :param show_viewer: Specifies whether to display a view of Vector's camera in a window. + :param show_3d_viewer: Specifies whether to display a 3D view of Vector's understanding of the world in a window. + :param behavior_control_level: Request control of Vector's behavior system at a specific level of control. Pass + :code:`None` if behavior control is not needed. + See :class:`ControlPriorityLevel` for more information.""" + + @functools.wraps(Robot.__init__) + def __init__(self, *args, **kwargs): + super(AsyncRobot, self).__init__(*args, **kwargs) + self._force_async = True diff --git a/anki_vector/util.py b/anki_vector/util.py index e5d6903..6d315bc 100644 --- a/anki_vector/util.py +++ b/anki_vector/util.py @@ -1,1132 +1,1134 @@ -# Copyright (c) 2018 Anki, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License in the file LICENSE.txt or at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -Utility functions and classes for the Vector SDK. -""" - -# __all__ should order by constants, event classes, other classes, functions. -__all__ = ['Angle', - 'BaseOverlay', - 'Component', - 'Distance', - 'ImageRect', - 'Matrix44', - 'Pose', - 'Position', - 'Quaternion', - 'RectangleOverlay', - 'Speed', - 'Vector2', - 'Vector3', - 'angle_z_to_quaternion', - 'block_while_none', - 'degrees', - 'distance_mm', - 'distance_inches', - 'get_class_logger', - 'parse_command_args', - 'radians', - 'setup_basic_logging', - 'speed_mmps'] - -import argparse -import configparser -from functools import wraps -import logging -import math -import os -from pathlib import Path -import sys -import time -from typing import Callable, Union - -from .exceptions import VectorConfigurationException, VectorPropertyValueNotReadyException -from .messaging import protocol - -try: - from PIL import Image, ImageDraw -except ImportError: - sys.exit("Cannot import from PIL: Do `pip3 install --user Pillow` to install") - - -def parse_command_args(parser: argparse.ArgumentParser = None): - """ - Parses command line arguments. - - Attempts to read the robot serial number from the command line arguments. If no serial number - is specified, we next attempt to read the robot serial number from environment variable ANKI_ROBOT_SERIAL. - If ANKI_ROBOT_SERIAL is specified, the value will be used as the robot's serial number. - - .. code-block:: python - - import anki_vector - - import argparse - - parser = argparse.ArgumentParser() - parser.add_argument("--new_param") - args = anki_vector.util.parse_command_args(parser) - - :param parser: To add new command line arguments, - pass an argparse parser with the new options - already defined. Leave empty to use the defaults. - """ - if parser is None: - parser = argparse.ArgumentParser() - parser.add_argument("-s", "--serial", nargs='?', default=os.environ.get('ANKI_ROBOT_SERIAL', None)) - return parser.parse_args() - - -def block_while_none(interval: float = 0.1, max_iterations: int = 50): - """Use this to denote a property that may need some delay before it appears. - - :param interval: how often to check if the property is no longer None - :param max_iterations: how many times to check the property before raising an error - - This will raise a :class:`VectorControlTimeoutException` if the property cannot be retrieved - before :attr:`max_iterations`. - """ - def blocker(func: Callable): - @wraps(func) - def wrapped(*args, **kwargs): - iterations = 0 - result = func(*args, **kwargs) - while result is None: - time.sleep(interval) - iterations += 1 - if iterations > max_iterations: - raise VectorPropertyValueNotReadyException() - result = func(*args, **kwargs) - return result - return wrapped - return blocker - - -def setup_basic_logging(custom_handler: logging.Handler = None, - general_log_level: str = None, - target: object = None): - """Helper to perform basic setup of the Python logger. - - :param custom_handler: provide an external logger for custom logging locations - :param general_log_level: 'DEBUG', 'INFO', 'WARN', 'ERROR' or an equivalent - constant from the :mod:`logging` module. If None then a - value will be read from the VECTOR_LOG_LEVEL environment variable. - :param target: The stream to send the log data to; defaults to stderr - """ - if general_log_level is None: - general_log_level = os.environ.get('VECTOR_LOG_LEVEL', logging.INFO) - - handler = custom_handler - if handler is None: - handler = logging.StreamHandler(stream=target) - formatter = logging.Formatter("%(asctime)s.%(msecs)03d %(name)+25s %(levelname)+7s %(message)s", - "%H:%M:%S") - handler.setFormatter(formatter) - - class LogCleanup(logging.Filter): # pylint: disable=too-few-public-methods - def filter(self, record): - # Drop 'anki_vector' from log messages - record.name = '.'.join(record.name.split('.')[1:]) - # Indent past informational chunk - record.msg = record.msg.replace("\n", f"\n{'':48}") - return True - handler.addFilter(LogCleanup()) - - vector_logger = logging.getLogger('anki_vector') - if not vector_logger.handlers: - vector_logger.addHandler(handler) - vector_logger.setLevel(general_log_level) - - -def get_class_logger(module: str, obj: object) -> logging.Logger: - """Helper to create logger for a given class (and module). - - .. testcode:: - - import anki_vector - - logger = anki_vector.util.get_class_logger("module_name", "object_name") - - :param module: The name of the module to which the object belongs. - :param obj: the object that owns the logger. - """ - return logging.getLogger(".".join([module, type(obj).__name__])) - - -class Vector2: - """Represents a 2D Vector (type/units aren't specified). - - :param x: X component - :param y: Y component - """ - - __slots__ = ('_x', '_y') - - def __init__(self, x: float, y: float): - self._x = float(x) - self._y = float(y) - - def set_to(self, rhs): - """Copy the x and y components of the given Vector2 instance. - - :param rhs: The right-hand-side of this assignment - the - source Vector2 to copy into this Vector2 instance. - """ - self._x = float(rhs.x) - self._y = float(rhs.y) - - @property - def x(self) -> float: - """The x component.""" - return self._x - - @property - def y(self) -> float: - """The y component.""" - return self._y - - @property - def x_y(self): - """tuple (float, float): The X, Y elements of the Vector2 (x,y)""" - return self._x, self._y - - def __repr__(self): - return "<%s x: %.2f y: %.2f>" % (self.__class__.__name__, self.x, self.y) - - def __add__(self, other): - if not isinstance(other, Vector2): - raise TypeError("Unsupported operand for + expected Vector2") - return Vector2(self.x + other.x, self.y + other.y) - - def __sub__(self, other): - if not isinstance(other, Vector2): - raise TypeError("Unsupported operand for - expected Vector2") - return Vector2(self.x - other.x, self.y - other.y) - - def __mul__(self, other): - if not isinstance(other, (int, float)): - raise TypeError("Unsupported operand for * expected number") - return Vector2(self.x * other, self.y * other) - - def __truediv__(self, other): - if not isinstance(other, (int, float)): - raise TypeError("Unsupported operand for / expected number") - return Vector2(self.x / other, self.y / other) - - -class Vector3: - """Represents a 3D Vector (type/units aren't specified). - - :param x: X component - :param y: Y component - :param z: Z component - """ - - __slots__ = ('_x', '_y', '_z') - - def __init__(self, x: float, y: float, z: float): - self._x = float(x) - self._y = float(y) - self._z = float(z) - - def set_to(self, rhs): - """Copy the x, y and z components of the given Vector3 instance. - - :param rhs: The right-hand-side of this assignment - the - source Vector3 to copy into this Vector3 instance. - """ - self._x = float(rhs.x) - self._y = float(rhs.y) - self._z = float(rhs.z) - - @property - def x(self) -> float: - """The x component.""" - return self._x - - @property - def y(self) -> float: - """The y component.""" - return self._y - - @property - def z(self) -> float: - """The z component.""" - return self._z - - @property - def magnitude_squared(self) -> float: - """float: The magnitude of the Vector3 instance""" - return self._x**2 + self._y**2 + self._z**2 - - @property - def magnitude(self) -> float: - """The magnitude of the Vector3 instance""" - return math.sqrt(self.magnitude_squared) - - @property - def normalized(self): - """A Vector3 instance with the same direction and unit magnitude""" - mag = self.magnitude - if mag == 0: - return Vector3(0, 0, 0) - return Vector3(self._x / mag, self._y / mag, self._z / mag) - - def dot(self, other): - """The dot product of this and another Vector3 instance""" - if not isinstance(other, Vector3): - raise TypeError("Unsupported argument for dot product, expected Vector3") - return self._x * other.x + self._y * other.y + self._z * other.z - - def cross(self, other): - """The cross product of this and another Vector3 instance""" - if not isinstance(other, Vector3): - raise TypeError("Unsupported argument for cross product, expected Vector3") - - return Vector3( - self._y * other.z - self._z * other.y, - self._z * other.x - self._x * other.z, - self._x * other.y - self._y * other.x) - - @property - def x_y_z(self): - """tuple (float, float, float): The X, Y, Z elements of the Vector3 (x,y,z)""" - return self._x, self._y, self._z - - def __repr__(self): - return f"<{self.__class__.__name__} x: {self.x:.2f} y: {self.y:.2f} z: {self.z:.2f}>" - - def __add__(self, other): - if not isinstance(other, Vector3): - raise TypeError("Unsupported operand for +, expected Vector3") - return Vector3(self.x + other.x, self.y + other.y, self.z + other.z) - - def __sub__(self, other): - if not isinstance(other, Vector3): - raise TypeError("Unsupported operand for -, expected Vector3") - return Vector3(self.x - other.x, self.y - other.y, self.z - other.z) - - def __mul__(self, other): - if not isinstance(other, (int, float)): - raise TypeError("Unsupported operand for * expected number") - return Vector3(self.x * other, self.y * other, self.z * other) - - def __truediv__(self, other): - if not isinstance(other, (int, float)): - raise TypeError("Unsupported operand for / expected number") - return Vector3(self.x / other, self.y / other, self.z / other) - - -class Angle: - """Represents an angle. - - Use the :func:`degrees` or :func:`radians` convenience methods to generate - an Angle instance. - - :param radians: The number of radians the angle should represent - (cannot be combined with ``degrees``) - :param degrees: The number of degress the angle should represent - (cannot be combined with ``radians``) - """ - - __slots__ = ('_radians') - - def __init__(self, radians: float = None, degrees: float = None): # pylint: disable=redefined-outer-name - if radians is None and degrees is None: - raise ValueError("Expected either the degrees or radians keyword argument") - if radians and degrees: - raise ValueError("Expected either the degrees or radians keyword argument, not both") - - if degrees is not None: - radians = degrees * math.pi / 180 - self._radians = float(radians) - - @property - def radians(self) -> float: # pylint: disable=redefined-outer-name - """The angle in radians.""" - return self._radians - - @property - def degrees(self) -> float: # pylint: disable=redefined-outer-name - """The angle in degrees.""" - return self._radians / math.pi * 180 - - def __repr__(self): - return f"<{self.__class__.__name__} Radians: {self.radians:.2f} Degrees: {self.degrees:.2f}>" - - def __add__(self, other): - if not isinstance(other, Angle): - raise TypeError("Unsupported type for + expected Angle") - return Angle(radians=(self.radians + other.radians)) - - def __sub__(self, other): - if not isinstance(other, Angle): - raise TypeError("Unsupported type for - expected Angle") - return Angle(radians=(self.radians - other.radians)) - - def __mul__(self, other): - if not isinstance(other, (int, float)): - raise TypeError("Unsupported type for * expected number") - return Angle(radians=(self.radians * other)) - - def __truediv__(self, other): - if not isinstance(other, (int, float)): - raise TypeError("Unsupported type for / expected number") - return radians(self.radians / other) - - def _cmp_int(self, other): - if not isinstance(other, Angle): - raise TypeError("Unsupported type for comparison expected Angle") - return self.radians - other.radians - - def __eq__(self, other): - return self._cmp_int(other) == 0 - - def __ne__(self, other): - return self._cmp_int(other) != 0 - - def __gt__(self, other): - return self._cmp_int(other) > 0 - - def __lt__(self, other): - return self._cmp_int(other) < 0 - - def __ge__(self, other): - return self._cmp_int(other) >= 0 - - def __le__(self, other): - return self._cmp_int(other) <= 0 - - @property - def abs_value(self): - """:class:`anki_vector.util.Angle`: The absolute value of the angle. - - If the Angle is positive then it returns a copy of this Angle, otherwise it returns -Angle. - """ - return Angle(radians=abs(self._radians)) - - -def angle_z_to_quaternion(angle_z: Angle): - """This function converts an angle in the z axis (Euler angle z component) to a quaternion. - - :param angle_z: The z axis angle. - - Returns: - q0, q1, q2, q3 (float, float, float, float): A tuple with all the members - of a quaternion defined by angle_z. - """ - - # Define the quaternion to be converted from a Euler angle (x,y,z) of 0,0,angle_z - # These equations have their original equations above, and simplified implemented - # q0 = cos(x/2)*cos(y/2)*cos(z/2) + sin(x/2)*sin(y/2)*sin(z/2) - q0 = math.cos(angle_z.radians / 2) - # q1 = sin(x/2)*cos(y/2)*cos(z/2) - cos(x/2)*sin(y/2)*sin(z/2) - q1 = 0 - # q2 = cos(x/2)*sin(y/2)*cos(z/2) + sin(x/2)*cos(y/2)*sin(z/2) - q2 = 0 - # q3 = cos(x/2)*cos(y/2)*sin(z/2) - sin(x/2)*sin(y/2)*cos(z/2) - q3 = math.sin(angle_z.radians / 2) - return q0, q1, q2, q3 - - -def degrees(degrees: float) -> Angle: # pylint: disable=redefined-outer-name - """An Angle instance set to the specified number of degrees.""" - return Angle(degrees=degrees) - - -def radians(radians: float) -> Angle: # pylint: disable=redefined-outer-name - """An Angle instance set to the specified number of radians.""" - return Angle(radians=radians) - - -class Matrix44: - """A 4x4 Matrix for representing the rotation and/or position of an object in the world. - - Can be generated from a :class:`Quaternion` for a pure rotation matrix, or - combined with a position for a full translation matrix, as done by - :meth:`Pose.to_matrix`. - """ - __slots__ = ('m00', 'm10', 'm20', 'm30', - 'm01', 'm11', 'm21', 'm31', - 'm02', 'm12', 'm22', 'm32', - 'm03', 'm13', 'm23', 'm33') - - def __init__(self, - m00: float, m10: float, m20: float, m30: float, - m01: float, m11: float, m21: float, m31: float, - m02: float, m12: float, m22: float, m32: float, - m03: float, m13: float, m23: float, m33: float): - self.m00 = float(m00) - self.m10 = float(m10) - self.m20 = float(m20) - self.m30 = float(m30) - - self.m01 = float(m01) - self.m11 = float(m11) - self.m21 = float(m21) - self.m31 = float(m31) - - self.m02 = float(m02) - self.m12 = float(m12) - self.m22 = float(m22) - self.m32 = float(m32) - - self.m03 = float(m03) - self.m13 = float(m13) - self.m23 = float(m23) - self.m33 = float(m33) - - def __repr__(self): - return ("<%s: " - "%.1f %.1f %.1f %.1f %.1f %.1f %.1f %.1f " - "%.1f %.1f %.1f %.1f %.1f %.1f %.1f %.1f>" % ( - self.__class__.__name__, *self.in_row_order)) - - @property - def tabulated_string(self) -> str: - """A multi-line string formatted with tabs to show the matrix contents.""" - return ("%.1f\t%.1f\t%.1f\t%.1f\n" - "%.1f\t%.1f\t%.1f\t%.1f\n" - "%.1f\t%.1f\t%.1f\t%.1f\n" - "%.1f\t%.1f\t%.1f\t%.1f" % self.in_row_order) - - @property - def in_row_order(self): - """tuple of 16 floats: The contents of the matrix in row order.""" - return self.m00, self.m01, self.m02, self.m03,\ - self.m10, self.m11, self.m12, self.m13,\ - self.m20, self.m21, self.m22, self.m23,\ - self.m30, self.m31, self.m32, self.m33 - - @property - def in_column_order(self): - """tuple of 16 floats: The contents of the matrix in column order.""" - return self.m00, self.m10, self.m20, self.m30,\ - self.m01, self.m11, self.m21, self.m31,\ - self.m02, self.m12, self.m22, self.m32,\ - self.m03, self.m13, self.m23, self.m33 - - @property - def forward_xyz(self): - """tuple of 3 floats: The x,y,z components representing the matrix's forward vector.""" - return self.m00, self.m01, self.m02 - - @property - def left_xyz(self): - """tuple of 3 floats: The x,y,z components representing the matrix's left vector.""" - return self.m10, self.m11, self.m12 - - @property - def up_xyz(self): - """tuple of 3 floats: The x,y,z components representing the matrix's up vector.""" - return self.m20, self.m21, self.m22 - - @property - def pos_xyz(self): - """tuple of 3 floats: The x,y,z components representing the matrix's position vector.""" - return self.m30, self.m31, self.m32 - - def set_forward(self, x: float, y: float, z: float): - """Set the x,y,z components representing the matrix's forward vector. - - :param x: The X component. - :param y: The Y component. - :param z: The Z component. - """ - self.m00 = float(x) - self.m01 = float(y) - self.m02 = float(z) - - def set_left(self, x: float, y: float, z: float): - """Set the x,y,z components representing the matrix's left vector. - - :param x: The X component. - :param y: The Y component. - :param z: The Z component. - """ - self.m10 = float(x) - self.m11 = float(y) - self.m12 = float(z) - - def set_up(self, x: float, y: float, z: float): - """Set the x,y,z components representing the matrix's up vector. - - :param x: The X component. - :param y: The Y component. - :param z: The Z component. - """ - self.m20 = float(x) - self.m21 = float(y) - self.m22 = float(z) - - def set_pos(self, x: float, y: float, z: float): - """Set the x,y,z components representing the matrix's position vector. - - :param x: The X component. - :param y: The Y component. - :param z: The Z component. - """ - self.m30 = float(x) - self.m31 = float(y) - self.m32 = float(z) - - -class Quaternion: - """Represents the rotation of an object in the world.""" - - __slots__ = ('_q0', '_q1', '_q2', '_q3') - - def __init__(self, q0: float = None, q1: float = None, q2: float = None, q3: float = None, angle_z: Angle = None): - is_quaternion = q0 is not None and q1 is not None and q2 is not None and q3 is not None - - if not is_quaternion and angle_z is None: - raise ValueError("Expected either the q0 q1 q2 and q3 or angle_z keyword arguments") - if is_quaternion and angle_z: - raise ValueError("Expected either the q0 q1 q2 and q3 or angle_z keyword argument," - "not both") - if angle_z is not None: - if not isinstance(angle_z, Angle): - raise TypeError("Unsupported type for angle_z expected Angle") - q0, q1, q2, q3 = angle_z_to_quaternion(angle_z) - - self._q0 = float(q0) - self._q1 = float(q1) - self._q2 = float(q2) - self._q3 = float(q3) - - @property - def q0(self) -> float: - """The q0 (w) value of the quaternion.""" - return self._q0 - - @property - def q1(self) -> float: - """The q1 (i) value of the quaternion.""" - return self._q1 - - @property - def q2(self) -> float: - """The q2 (j) value of the quaternion.""" - return self._q2 - - @property - def q3(self) -> float: - """The q3 (k) value of the quaternion.""" - return self._q3 - - @property - def angle_z(self) -> Angle: - """An Angle instance representing the z Euler component of the object's rotation. - - Defined as the rotation in the z axis. - """ - q0, q1, q2, q3 = self.q0_q1_q2_q3 - return Angle(radians=math.atan2(2 * (q1 * q2 + q0 * q3), 1 - 2 * (q2**2 + q3**2))) - - @property - def q0_q1_q2_q3(self): - """tuple of float: Contains all elements of the quaternion (q0,q1,q2,q3)""" - return self._q0, self._q1, self._q2, self._q3 - - def to_matrix(self, pos_x: float = 0.0, pos_y: float = 0.0, pos_z: float = 0.0): - """Convert the Quaternion to a 4x4 matrix representing this rotation. - - A position can also be provided to generate a full translation matrix. - - :param pos_x: The x component for the position. - :param pos_y: The y component for the position. - :param pos_z: The z component for the position. - - Returns: - :class:`anki_vector.util.Matrix44`: A matrix representing this Quaternion's - rotation, with the provided position (which defaults to 0,0,0). - """ - # See https://en.wikipedia.org/wiki/Quaternions_and_spatial_rotation - q0q0 = self.q0 * self.q0 - q1q1 = self.q1 * self.q1 - q2q2 = self.q2 * self.q2 - q3q3 = self.q3 * self.q3 - - q0x2 = self.q0 * 2.0 # saves 2 multiplies - q0q1x2 = q0x2 * self.q1 - q0q2x2 = q0x2 * self.q2 - q0q3x2 = q0x2 * self.q3 - q1x2 = self.q1 * 2.0 # saves 1 multiply - q1q2x2 = q1x2 * self.q2 - q1q3x2 = q1x2 * self.q3 - q2q3x2 = 2.0 * self.q2 * self.q3 - - m00 = (q0q0 + q1q1 - q2q2 - q3q3) - m01 = (q1q2x2 + q0q3x2) - m02 = (q1q3x2 - q0q2x2) - - m10 = (q1q2x2 - q0q3x2) - m11 = (q0q0 - q1q1 + q2q2 - q3q3) - m12 = (q0q1x2 + q2q3x2) - - m20 = (q0q2x2 + q1q3x2) - m21 = (q2q3x2 - q0q1x2) - m22 = (q0q0 - q1q1 - q2q2 + q3q3) - - return Matrix44(m00, m10, m20, float(pos_x), - m01, m11, m21, float(pos_y), - m02, m12, m22, float(pos_z), - 0.0, 0.0, 0.0, 1.0) - - def __repr__(self): - return (f"<{self.__class__.__name__} q0: {self.q0:.2f} q1: {self.q1:.2f}" - f" q2: {self.q2:.2f} q3: {self.q3:.2f} {self.angle_z}>") - - -class Position(Vector3): - """Represents the position of an object in the world. - - A position consists of its x, y and z values in millimeters. - - :param x: X position in millimeters - :param y: Y position in millimeters - :param z: Z position in millimeters - """ - __slots__ = () - - -class Pose: - """Represents where an object is in the world. - - Whenever Vector is delocalized (i.e. whenever Vector no longer knows - where he is - e.g. when he's picked up), Vector creates a new pose starting at - (0,0,0) with no rotation, with origin_id incremented to show that these poses - cannot be compared with earlier ones. As Vector drives around, his pose (and the - pose of other objects he observes - e.g. faces, his LightCube, charger, etc.) is relative to this - initial position and orientation. - - The coordinate space is relative to Vector, where Vector's origin is the - point on the ground between Vector's two front wheels. The X axis is Vector's forward direction, - the Y axis is to Vector's left, and the Z axis is up. - - Only poses of the same origin_id can safely be compared or operated on. - - .. testcode:: - - import anki_vector - from anki_vector.util import degrees, Pose - - with anki_vector.Robot() as robot: - pose = Pose(x=50, y=0, z=0, angle_z=anki_vector.util.Angle(degrees=0)) - robot.behavior.go_to_pose(pose) - """ - __slots__ = ('_position', '_rotation', '_origin_id') - - def __init__(self, x: float, y: float, z: float, q0: float = None, q1: float = None, q2: float = None, q3: float = None, - angle_z: Angle = None, origin_id: int = -1): - self._position = Position(x, y, z) - self._rotation = Quaternion(q0, q1, q2, q3, angle_z) - self._origin_id = origin_id - - @property - def position(self) -> Position: - """The position component of this pose.""" - return self._position - - @property - def rotation(self) -> Quaternion: - """The rotation component of this pose.""" - return self._rotation - - @property - def origin_id(self) -> int: - """An ID maintained by the robot which represents which coordinate frame this pose is in.""" - return self._origin_id - - def __repr__(self): - return (f"<{self.__class__.__name__}: {self._position}" - f" {self._rotation} >") - - def define_pose_relative_this(self, new_pose): - """Creates a new pose such that new_pose's origin is now at the location of this pose. - - :param anki_vector.util.Pose new_pose: The pose which origin is being changed. - - Returns: - A :class:`anki_vector.util.Pose` object for which the origin was this pose's origin. - """ - if not isinstance(new_pose, Pose): - raise TypeError("Unsupported type for new_origin, must be of type Pose") - x, y, z = self.position.x_y_z - angle_z = self.rotation.angle_z - new_x, new_y, new_z = new_pose.position.x_y_z - new_angle_z = new_pose.rotation.angle_z - - cos_angle = math.cos(angle_z.radians) - sin_angle = math.sin(angle_z.radians) - res_x = x + (cos_angle * new_x) - (sin_angle * new_y) - res_y = y + (sin_angle * new_x) + (cos_angle * new_y) - res_z = z + new_z - res_angle = angle_z + new_angle_z - return Pose(res_x, - res_y, - res_z, - angle_z=res_angle, - origin_id=self._origin_id) - - @property - def is_valid(self) -> bool: - """True if this is a valid, usable pose.""" - return self.origin_id >= 0 - - def is_comparable(self, other_pose) -> bool: - """Checks whether these two poses are comparable. - - Poses are comparable if they're valid and having matching origin IDs. - - :param other_pose: The other pose to compare against. Type is Pose. - - Returns: - True if the two poses are comparable, False otherwise. - """ - return (self.is_valid and other_pose.is_valid - and (self.origin_id == other_pose.origin_id)) - - def to_matrix(self) -> Matrix44: - """Convert the Pose to a Matrix44. - - Returns: - A matrix representing this Pose's position and rotation. - """ - return self.rotation.to_matrix(*self.position.x_y_z) - - def to_proto_pose_struct(self) -> protocol.PoseStruct: - """Converts the Pose into the robot's messaging pose format. - """ - return protocol.PoseStruct( - x=self._position.x, - y=self._position.y, - z=self._position.z, - q0=self._rotation.q0, - q1=self._rotation.q1, - q2=self._rotation.q2, - q3=self._rotation.q3, - origin_id=self._origin_id) - - -class ImageRect: - '''Defines a bounding box within an image frame. - - This is used when objects and faces are observed to denote where in - the robot's camera view the object or face actually appears. It's then - used by the annotate module to show an outline of a box around - the object or face. - ''' - - __slots__ = ('_x_top_left', '_y_top_left', '_width', '_height') - - def __init__(self, x_top_left: float, y_top_left: float, width: float, height: float): - self._x_top_left = float(x_top_left) - self._y_top_left = float(y_top_left) - self._width = float(width) - self._height = float(height) - - @property - def x_top_left(self) -> float: - """The top left x value of where the object was last visible within Vector's camera view.""" - return self._x_top_left - - @property - def y_top_left(self) -> float: - """The top left y value of where the object was last visible within Vector's camera view.""" - return self._y_top_left - - @property - def width(self) -> float: - """The width of the object from when it was last visible within Vector's camera view.""" - return self._width - - @property - def height(self) -> float: - """The height of the object from when it was last visible within Vector's camera view.""" - return self._height - - def scale_by(self, scale_multiplier: Union[int, float]) -> None: - """Scales the image rectangle by the multiplier provided.""" - if not isinstance(scale_multiplier, (int, float)): - raise TypeError("Unsupported operand for * expected number") - self._x_top_left *= scale_multiplier - self._y_top_left *= scale_multiplier - self._width *= scale_multiplier - self._height *= scale_multiplier - - -class Distance: - """Represents a distance. - - The class allows distances to be returned in either millimeters or inches. - - Use the :func:`distance_inches` or :func:`distance_mm` convenience methods to generate - a Distance instance. - - :param distance_mm: The number of millimeters the distance should - represent (cannot be combined with ``distance_inches``). - :param distance_inches: The number of inches the distance should - represent (cannot be combined with ``distance_mm``). - """ - - __slots__ = ('_distance_mm') - - def __init__(self, distance_mm: float = None, distance_inches: float = None): # pylint: disable=redefined-outer-name - if distance_mm is None and distance_inches is None: - raise ValueError("Expected either the distance_mm or distance_inches keyword argument") - if distance_mm and distance_inches: - raise ValueError("Expected either the distance_mm or distance_inches keyword argument, not both") - - if distance_inches is not None: - distance_mm = distance_inches * 25.4 - self._distance_mm = float(distance_mm) - - def __repr__(self): - return "<%s %.2f mm (%.2f inches)>" % (self.__class__.__name__, self.distance_mm, self.distance_inches) - - def __add__(self, other): - if not isinstance(other, Distance): - raise TypeError("Unsupported operand for + expected Distance") - return distance_mm(self.distance_mm + other.distance_mm) - - def __sub__(self, other): - if not isinstance(other, Distance): - raise TypeError("Unsupported operand for - expected Distance") - return distance_mm(self.distance_mm - other.distance_mm) - - def __mul__(self, other): - if not isinstance(other, (int, float)): - raise TypeError("Unsupported operand for * expected number") - return distance_mm(self.distance_mm * other) - - def __truediv__(self, other): - if not isinstance(other, (int, float)): - raise TypeError("Unsupported operand for / expected number") - return distance_mm(self.distance_mm / other) - - @property - def distance_mm(self) -> float: # pylint: disable=redefined-outer-name - """The distance in millimeters""" - return self._distance_mm - - @property - def distance_inches(self) -> float: # pylint: disable=redefined-outer-name - return self._distance_mm / 25.4 - - -def distance_mm(distance_mm: float): # pylint: disable=redefined-outer-name - """Returns an :class:`anki_vector.util.Distance` instance set to the specified number of millimeters.""" - return Distance(distance_mm=distance_mm) - - -def distance_inches(distance_inches: float): # pylint: disable=redefined-outer-name - """Returns an :class:`anki_vector.util.Distance` instance set to the specified number of inches.""" - return Distance(distance_inches=distance_inches) - - -class Speed: - """Represents a speed. - - This class allows speeds to be measured in millimeters per second. - - The maximum speed is 220 mm/s and is clamped internally. - - Use :func:`speed_mmps` convenience methods to generate - a Speed instance. - - :param speed_mmps: The number of millimeters per second the speed - should represent. - """ - - __slots__ = ('_speed_mmps') - - def __init__(self, speed_mmps: float = None): # pylint: disable=redefined-outer-name - if speed_mmps is None: - raise ValueError("Expected speed_mmps keyword argument") - self._speed_mmps = float(speed_mmps) - - def __repr__(self): - return "<%s %.2f mmps>" % (self.__class__.__name__, self.speed_mmps) - - def __add__(self, other): - if not isinstance(other, Speed): - raise TypeError("Unsupported operand for + expected Speed") - return speed_mmps(self.speed_mmps + other.speed_mmps) - - def __sub__(self, other): - if not isinstance(other, Speed): - raise TypeError("Unsupported operand for - expected Speed") - return speed_mmps(self.speed_mmps - other.speed_mmps) - - def __mul__(self, other): - if not isinstance(other, (int, float)): - raise TypeError("Unsupported operand for * expected number") - return speed_mmps(self.speed_mmps * other) - - def __truediv__(self, other): - if not isinstance(other, (int, float)): - raise TypeError("Unsupported operand for / expected number") - return speed_mmps(self.speed_mmps / other) - - @property - def speed_mmps(self: float) -> float: # pylint: disable=redefined-outer-name - """The speed in millimeters per second (mmps).""" - return self._speed_mmps - - -def speed_mmps(speed_mmps: float): # pylint: disable=redefined-outer-name - """:class:`anki_vector.util.Speed` instance set to the specified millimeters per second speed.""" - return Speed(speed_mmps=speed_mmps) - - -class BaseOverlay: - """A base overlay is used as a base class for other forms of overlays that can be drawn on top of an image. - - :param line_thickness: The thickness of the line being drawn. - :param line_color: The color of the line to be drawn. - """ - - def __init__(self, line_thickness: int, line_color: tuple): - self._line_thickness: int = line_thickness - self._line_color: tuple = line_color - - @property - def line_thickness(self) -> int: - """The thickness of the line being drawn.""" - return self._line_thickness - - @property - def line_color(self) -> tuple: - """The color of the line to be drawn.""" - return self._line_color - - -class RectangleOverlay(BaseOverlay): - """A rectangle that can be drawn on top of a given image. - - :param width: The width of the rectangle to be drawn. - :param height: The height of the rectangle to be drawn. - :param line_thickness: The thickness of the line being drawn. - :param line_color: The color of the line to be drawn. - """ - - # @TODO Implement overlay using an ImageRect rather than a raw width & height - def __init__(self, width: int, height: int, line_thickness: int = 5, line_color: tuple = (255, 0, 0)): - super().__init__(line_thickness, line_color) - self._width: int = width - self._height: int = height - - @property - def width(self) -> int: - """The width of the rectangle to be drawn.""" - return self._width - - @property - def height(self) -> int: - """The height of the rectangle to be drawn.""" - return self._height - - def apply_overlay(self, image: Image.Image) -> None: - """Draw a rectangle on top of the given image.""" - d = ImageDraw.Draw(image) - - image_width, image_height = image.size - remaining_width = image_width - self.width - remaining_height = image_height - self.height - x1, y1 = remaining_width // 2, remaining_height // 2 - x2, y2 = (image_width - (remaining_width // 2)), (image_height - (remaining_height // 2)) - - for i in range(0, self.line_thickness): - d.rectangle([x1 + i, y1 + i, x2 - i, y2 - i], outline=self.line_color) - - -class Component: - """ Base class for all components.""" - - def __init__(self, robot): - self.logger = get_class_logger(__name__, self) - self._robot = robot - - @property - def robot(self): - return self._robot - - @property - def conn(self): - return self._robot.conn - - @property - def force_async(self): - return self._robot.force_async - - @property - def grpc_interface(self): - """A direct reference to the connected aiogrpc interface. - """ - return self._robot.conn.grpc_interface - - -def read_configuration(serial: str, name: str, logger: logging.Logger) -> dict: - """Open the default conf file, and read it into a :class:`configparser.ConfigParser` - If :code:`serial is not None`, this method will try to find a configuration with serial - number :code:`serial`, and raise an exception otherwise. If :code:`serial is None` and - :code:`name is not None`, this method will try to find a configuration which matches - the provided name, and raise an exception otherwise. If both :code:`serial is None` and - :code:`name is None`, this method will return a configuration if exactly `1` exists, but - if multiple configurations exists, it will raise an exception. - - :param serial: Vector's serial number - :param name: Vector's name - """ - home = Path.home() / ".anki_vector" - conf_file = str(home / "sdk_config.ini") - parser = configparser.ConfigParser(strict=False) - parser.read(conf_file) - - sections = parser.sections() - if not sections: - raise VectorConfigurationException('Could not find the sdk configuration file. Please run `python3 -m anki_vector.configure` to set up your Vector for SDK usage.') - elif (serial is None) and (name is None): - if len(sections) == 1: - serial = sections[0] - logger.warning("No serial number or name provided. Automatically selecting {}".format(serial)) - else: - raise VectorConfigurationException("Found multiple robot serial numbers. " - "Please provide the serial number or name of the Robot you want to control.\n\n" - "Example: ./01_hello_world.py --serial {{robot_serial_number}}") - - config = {k.lower(): v for k, v in parser.items()} - - if serial is not None: - serial = serial.lower() - try: - return config[serial] - except KeyError: - raise VectorConfigurationException("Could not find matching robot info for given serial number: {}. " - "Please check your serial number is correct.\n\n" - "Example: ./01_hello_world.py --serial {{robot_serial_number}}", serial) - else: - for keySerial in config: - for key in config[keySerial]: - if config[keySerial][key] == name: - return config[keySerial] - if config[keySerial][key].lower() == name.lower(): - logger.warning("Using case-insensitive name match found in config. Set 'name' field to match 'Vector-A1B2' format.") - return config[keySerial] - - raise VectorConfigurationException("Could not find matching robot info for given name: {}. " - "Please check your name is correct.\n\n" - "Example: ./01_hello_world.py --name {{robot_name}}", name) +# Copyright (c) 2018 Anki, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License in the file LICENSE.txt or at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Utility functions and classes for the Vector SDK. +""" + +# __all__ should order by constants, event classes, other classes, functions. +__all__ = ['Angle', + 'BaseOverlay', + 'Component', + 'Distance', + 'ImageRect', + 'Matrix44', + 'Pose', + 'Position', + 'Quaternion', + 'RectangleOverlay', + 'Speed', + 'Vector2', + 'Vector3', + 'angle_z_to_quaternion', + 'block_while_none', + 'degrees', + 'distance_mm', + 'distance_inches', + 'get_class_logger', + 'parse_command_args', + 'radians', + 'setup_basic_logging', + 'speed_mmps'] + +import argparse +import configparser +from functools import wraps +import logging +import math +import os +from pathlib import Path +import sys +import time +from typing import Callable, Union + +from .exceptions import VectorConfigurationException, VectorPropertyValueNotReadyException +from .messaging import protocol + +try: + from PIL import Image, ImageDraw +except ImportError: + sys.exit("Cannot import from PIL: Do `pip3 install --user Pillow` to install") + + +def parse_command_args(parser: argparse.ArgumentParser = None): + """ + Parses command line arguments. + + Attempts to read the robot serial number from the command line arguments. If no serial number + is specified, we next attempt to read the robot serial number from environment variable ANKI_ROBOT_SERIAL. + If ANKI_ROBOT_SERIAL is specified, the value will be used as the robot's serial number. + + .. code-block:: python + + import anki_vector + + import argparse + + parser = argparse.ArgumentParser() + parser.add_argument("--new_param") + args = anki_vector.util.parse_command_args(parser) + + :param parser: To add new command line arguments, + pass an argparse parser with the new options + already defined. Leave empty to use the defaults. + """ + if parser is None: + parser = argparse.ArgumentParser() + parser.add_argument("-s", "--serial", nargs='?', default=os.environ.get('ANKI_ROBOT_SERIAL', None)) + return parser.parse_args() + + +def block_while_none(interval: float = 0.1, max_iterations: int = 50): + """Use this to denote a property that may need some delay before it appears. + + :param interval: how often to check if the property is no longer None + :param max_iterations: how many times to check the property before raising an error + + This will raise a :class:`VectorControlTimeoutException` if the property cannot be retrieved + before :attr:`max_iterations`. + """ + def blocker(func: Callable): + @wraps(func) + def wrapped(*args, **kwargs): + iterations = 0 + result = func(*args, **kwargs) + while result is None: + time.sleep(interval) + iterations += 1 + if iterations > max_iterations: + raise VectorPropertyValueNotReadyException() + result = func(*args, **kwargs) + return result + return wrapped + return blocker + + +def setup_basic_logging(custom_handler: logging.Handler = None, + general_log_level: str = None, + target: object = None): + """Helper to perform basic setup of the Python logger. + + :param custom_handler: provide an external logger for custom logging locations + :param general_log_level: 'DEBUG', 'INFO', 'WARN', 'ERROR' or an equivalent + constant from the :mod:`logging` module. If None then a + value will be read from the VECTOR_LOG_LEVEL environment variable. + :param target: The stream to send the log data to; defaults to stderr + """ + if general_log_level is None: + general_log_level = os.environ.get('VECTOR_LOG_LEVEL', logging.INFO) + + handler = custom_handler + if handler is None: + handler = logging.StreamHandler(stream=target) + formatter = logging.Formatter("%(asctime)s.%(msecs)03d %(name)+25s %(levelname)+7s %(message)s", + "%H:%M:%S") + handler.setFormatter(formatter) + + class LogCleanup(logging.Filter): # pylint: disable=too-few-public-methods + def filter(self, record): + # Drop 'anki_vector' from log messages + record.name = '.'.join(record.name.split('.')[1:]) + # Indent past informational chunk + record.msg = record.msg.replace("\n", f"\n{'':48}") + return True + handler.addFilter(LogCleanup()) + + vector_logger = logging.getLogger('anki_vector') + if not vector_logger.handlers: + vector_logger.addHandler(handler) + vector_logger.setLevel(general_log_level) + + +def get_class_logger(module: str, obj: object) -> logging.Logger: + """Helper to create logger for a given class (and module). + + .. testcode:: + + import anki_vector + + logger = anki_vector.util.get_class_logger("module_name", "object_name") + + :param module: The name of the module to which the object belongs. + :param obj: the object that owns the logger. + """ + return logging.getLogger(".".join([module, type(obj).__name__])) + + +class Vector2: + """Represents a 2D Vector (type/units aren't specified). + + :param x: X component + :param y: Y component + """ + + __slots__ = ('_x', '_y') + + def __init__(self, x: float, y: float): + self._x = float(x) + self._y = float(y) + + def set_to(self, rhs): + """Copy the x and y components of the given Vector2 instance. + + :param rhs: The right-hand-side of this assignment - the + source Vector2 to copy into this Vector2 instance. + """ + self._x = float(rhs.x) + self._y = float(rhs.y) + + @property + def x(self) -> float: + """The x component.""" + return self._x + + @property + def y(self) -> float: + """The y component.""" + return self._y + + @property + def x_y(self): + """tuple (float, float): The X, Y elements of the Vector2 (x,y)""" + return self._x, self._y + + def __repr__(self): + return "<%s x: %.2f y: %.2f>" % (self.__class__.__name__, self.x, self.y) + + def __add__(self, other): + if not isinstance(other, Vector2): + raise TypeError("Unsupported operand for + expected Vector2") + return Vector2(self.x + other.x, self.y + other.y) + + def __sub__(self, other): + if not isinstance(other, Vector2): + raise TypeError("Unsupported operand for - expected Vector2") + return Vector2(self.x - other.x, self.y - other.y) + + def __mul__(self, other): + if not isinstance(other, (int, float)): + raise TypeError("Unsupported operand for * expected number") + return Vector2(self.x * other, self.y * other) + + def __truediv__(self, other): + if not isinstance(other, (int, float)): + raise TypeError("Unsupported operand for / expected number") + return Vector2(self.x / other, self.y / other) + + +class Vector3: + """Represents a 3D Vector (type/units aren't specified). + + :param x: X component + :param y: Y component + :param z: Z component + """ + + __slots__ = ('_x', '_y', '_z') + + def __init__(self, x: float, y: float, z: float): + self._x = float(x) + self._y = float(y) + self._z = float(z) + + def set_to(self, rhs): + """Copy the x, y and z components of the given Vector3 instance. + + :param rhs: The right-hand-side of this assignment - the + source Vector3 to copy into this Vector3 instance. + """ + self._x = float(rhs.x) + self._y = float(rhs.y) + self._z = float(rhs.z) + + @property + def x(self) -> float: + """The x component.""" + return self._x + + @property + def y(self) -> float: + """The y component.""" + return self._y + + @property + def z(self) -> float: + """The z component.""" + return self._z + + @property + def magnitude_squared(self) -> float: + """float: The magnitude of the Vector3 instance""" + return self._x**2 + self._y**2 + self._z**2 + + @property + def magnitude(self) -> float: + """The magnitude of the Vector3 instance""" + return math.sqrt(self.magnitude_squared) + + @property + def normalized(self): + """A Vector3 instance with the same direction and unit magnitude""" + mag = self.magnitude + if mag == 0: + return Vector3(0, 0, 0) + return Vector3(self._x / mag, self._y / mag, self._z / mag) + + def dot(self, other): + """The dot product of this and another Vector3 instance""" + if not isinstance(other, Vector3): + raise TypeError("Unsupported argument for dot product, expected Vector3") + return self._x * other.x + self._y * other.y + self._z * other.z + + def cross(self, other): + """The cross product of this and another Vector3 instance""" + if not isinstance(other, Vector3): + raise TypeError("Unsupported argument for cross product, expected Vector3") + + return Vector3( + self._y * other.z - self._z * other.y, + self._z * other.x - self._x * other.z, + self._x * other.y - self._y * other.x) + + @property + def x_y_z(self): + """tuple (float, float, float): The X, Y, Z elements of the Vector3 (x,y,z)""" + return self._x, self._y, self._z + + def __repr__(self): + return f"<{self.__class__.__name__} x: {self.x:.2f} y: {self.y:.2f} z: {self.z:.2f}>" + + def __add__(self, other): + if not isinstance(other, Vector3): + raise TypeError("Unsupported operand for +, expected Vector3") + return Vector3(self.x + other.x, self.y + other.y, self.z + other.z) + + def __sub__(self, other): + if not isinstance(other, Vector3): + raise TypeError("Unsupported operand for -, expected Vector3") + return Vector3(self.x - other.x, self.y - other.y, self.z - other.z) + + def __mul__(self, other): + if not isinstance(other, (int, float)): + raise TypeError("Unsupported operand for * expected number") + return Vector3(self.x * other, self.y * other, self.z * other) + + def __truediv__(self, other): + if not isinstance(other, (int, float)): + raise TypeError("Unsupported operand for / expected number") + return Vector3(self.x / other, self.y / other, self.z / other) + + +class Angle: + """Represents an angle. + + Use the :func:`degrees` or :func:`radians` convenience methods to generate + an Angle instance. + + :param radians: The number of radians the angle should represent + (cannot be combined with ``degrees``) + :param degrees: The number of degress the angle should represent + (cannot be combined with ``radians``) + """ + + __slots__ = ('_radians') + + def __init__(self, radians: float = None, degrees: float = None): # pylint: disable=redefined-outer-name + if radians is None and degrees is None: + raise ValueError("Expected either the degrees or radians keyword argument") + if radians and degrees: + raise ValueError("Expected either the degrees or radians keyword argument, not both") + + if degrees is not None: + radians = degrees * math.pi / 180 + self._radians = float(radians) + + @property + def radians(self) -> float: # pylint: disable=redefined-outer-name + """The angle in radians.""" + return self._radians + + @property + def degrees(self) -> float: # pylint: disable=redefined-outer-name + """The angle in degrees.""" + return self._radians / math.pi * 180 + + def __repr__(self): + return f"<{self.__class__.__name__} Radians: {self.radians:.2f} Degrees: {self.degrees:.2f}>" + + def __add__(self, other): + if not isinstance(other, Angle): + raise TypeError("Unsupported type for + expected Angle") + return Angle(radians=(self.radians + other.radians)) + + def __sub__(self, other): + if not isinstance(other, Angle): + raise TypeError("Unsupported type for - expected Angle") + return Angle(radians=(self.radians - other.radians)) + + def __mul__(self, other): + if not isinstance(other, (int, float)): + raise TypeError("Unsupported type for * expected number") + return Angle(radians=(self.radians * other)) + + def __truediv__(self, other): + if not isinstance(other, (int, float)): + raise TypeError("Unsupported type for / expected number") + return radians(self.radians / other) + + def _cmp_int(self, other): + if not isinstance(other, Angle): + raise TypeError("Unsupported type for comparison expected Angle") + return self.radians - other.radians + + def __eq__(self, other): + return self._cmp_int(other) == 0 + + def __ne__(self, other): + return self._cmp_int(other) != 0 + + def __gt__(self, other): + return self._cmp_int(other) > 0 + + def __lt__(self, other): + return self._cmp_int(other) < 0 + + def __ge__(self, other): + return self._cmp_int(other) >= 0 + + def __le__(self, other): + return self._cmp_int(other) <= 0 + + @property + def abs_value(self): + """:class:`anki_vector.util.Angle`: The absolute value of the angle. + + If the Angle is positive then it returns a copy of this Angle, otherwise it returns -Angle. + """ + return Angle(radians=abs(self._radians)) + + +def angle_z_to_quaternion(angle_z: Angle): + """This function converts an angle in the z axis (Euler angle z component) to a quaternion. + + :param angle_z: The z axis angle. + + Returns: + q0, q1, q2, q3 (float, float, float, float): A tuple with all the members + of a quaternion defined by angle_z. + """ + + # Define the quaternion to be converted from a Euler angle (x,y,z) of 0,0,angle_z + # These equations have their original equations above, and simplified implemented + # q0 = cos(x/2)*cos(y/2)*cos(z/2) + sin(x/2)*sin(y/2)*sin(z/2) + q0 = math.cos(angle_z.radians / 2) + # q1 = sin(x/2)*cos(y/2)*cos(z/2) - cos(x/2)*sin(y/2)*sin(z/2) + q1 = 0 + # q2 = cos(x/2)*sin(y/2)*cos(z/2) + sin(x/2)*cos(y/2)*sin(z/2) + q2 = 0 + # q3 = cos(x/2)*cos(y/2)*sin(z/2) - sin(x/2)*sin(y/2)*cos(z/2) + q3 = math.sin(angle_z.radians / 2) + return q0, q1, q2, q3 + + +def degrees(degrees: float) -> Angle: # pylint: disable=redefined-outer-name + """An Angle instance set to the specified number of degrees.""" + return Angle(degrees=degrees) + + +def radians(radians: float) -> Angle: # pylint: disable=redefined-outer-name + """An Angle instance set to the specified number of radians.""" + return Angle(radians=radians) + + +class Matrix44: + """A 4x4 Matrix for representing the rotation and/or position of an object in the world. + + Can be generated from a :class:`Quaternion` for a pure rotation matrix, or + combined with a position for a full translation matrix, as done by + :meth:`Pose.to_matrix`. + """ + __slots__ = ('m00', 'm10', 'm20', 'm30', + 'm01', 'm11', 'm21', 'm31', + 'm02', 'm12', 'm22', 'm32', + 'm03', 'm13', 'm23', 'm33') + + def __init__(self, + m00: float, m10: float, m20: float, m30: float, + m01: float, m11: float, m21: float, m31: float, + m02: float, m12: float, m22: float, m32: float, + m03: float, m13: float, m23: float, m33: float): + self.m00 = float(m00) + self.m10 = float(m10) + self.m20 = float(m20) + self.m30 = float(m30) + + self.m01 = float(m01) + self.m11 = float(m11) + self.m21 = float(m21) + self.m31 = float(m31) + + self.m02 = float(m02) + self.m12 = float(m12) + self.m22 = float(m22) + self.m32 = float(m32) + + self.m03 = float(m03) + self.m13 = float(m13) + self.m23 = float(m23) + self.m33 = float(m33) + + def __repr__(self): + return ("<%s: " + "%.1f %.1f %.1f %.1f %.1f %.1f %.1f %.1f " + "%.1f %.1f %.1f %.1f %.1f %.1f %.1f %.1f>" % ( + self.__class__.__name__, *self.in_row_order)) + + @property + def tabulated_string(self) -> str: + """A multi-line string formatted with tabs to show the matrix contents.""" + return ("%.1f\t%.1f\t%.1f\t%.1f\n" + "%.1f\t%.1f\t%.1f\t%.1f\n" + "%.1f\t%.1f\t%.1f\t%.1f\n" + "%.1f\t%.1f\t%.1f\t%.1f" % self.in_row_order) + + @property + def in_row_order(self): + """tuple of 16 floats: The contents of the matrix in row order.""" + return self.m00, self.m01, self.m02, self.m03,\ + self.m10, self.m11, self.m12, self.m13,\ + self.m20, self.m21, self.m22, self.m23,\ + self.m30, self.m31, self.m32, self.m33 + + @property + def in_column_order(self): + """tuple of 16 floats: The contents of the matrix in column order.""" + return self.m00, self.m10, self.m20, self.m30,\ + self.m01, self.m11, self.m21, self.m31,\ + self.m02, self.m12, self.m22, self.m32,\ + self.m03, self.m13, self.m23, self.m33 + + @property + def forward_xyz(self): + """tuple of 3 floats: The x,y,z components representing the matrix's forward vector.""" + return self.m00, self.m01, self.m02 + + @property + def left_xyz(self): + """tuple of 3 floats: The x,y,z components representing the matrix's left vector.""" + return self.m10, self.m11, self.m12 + + @property + def up_xyz(self): + """tuple of 3 floats: The x,y,z components representing the matrix's up vector.""" + return self.m20, self.m21, self.m22 + + @property + def pos_xyz(self): + """tuple of 3 floats: The x,y,z components representing the matrix's position vector.""" + return self.m30, self.m31, self.m32 + + def set_forward(self, x: float, y: float, z: float): + """Set the x,y,z components representing the matrix's forward vector. + + :param x: The X component. + :param y: The Y component. + :param z: The Z component. + """ + self.m00 = float(x) + self.m01 = float(y) + self.m02 = float(z) + + def set_left(self, x: float, y: float, z: float): + """Set the x,y,z components representing the matrix's left vector. + + :param x: The X component. + :param y: The Y component. + :param z: The Z component. + """ + self.m10 = float(x) + self.m11 = float(y) + self.m12 = float(z) + + def set_up(self, x: float, y: float, z: float): + """Set the x,y,z components representing the matrix's up vector. + + :param x: The X component. + :param y: The Y component. + :param z: The Z component. + """ + self.m20 = float(x) + self.m21 = float(y) + self.m22 = float(z) + + def set_pos(self, x: float, y: float, z: float): + """Set the x,y,z components representing the matrix's position vector. + + :param x: The X component. + :param y: The Y component. + :param z: The Z component. + """ + self.m30 = float(x) + self.m31 = float(y) + self.m32 = float(z) + + +class Quaternion: + """Represents the rotation of an object in the world.""" + + __slots__ = ('_q0', '_q1', '_q2', '_q3') + + def __init__(self, q0: float = None, q1: float = None, q2: float = None, q3: float = None, angle_z: Angle = None): + is_quaternion = q0 is not None and q1 is not None and q2 is not None and q3 is not None + + if not is_quaternion and angle_z is None: + raise ValueError("Expected either the q0 q1 q2 and q3 or angle_z keyword arguments") + if is_quaternion and angle_z: + raise ValueError("Expected either the q0 q1 q2 and q3 or angle_z keyword argument," + "not both") + if angle_z is not None: + if not isinstance(angle_z, Angle): + raise TypeError("Unsupported type for angle_z expected Angle") + q0, q1, q2, q3 = angle_z_to_quaternion(angle_z) + + self._q0 = float(q0) + self._q1 = float(q1) + self._q2 = float(q2) + self._q3 = float(q3) + + @property + def q0(self) -> float: + """The q0 (w) value of the quaternion.""" + return self._q0 + + @property + def q1(self) -> float: + """The q1 (i) value of the quaternion.""" + return self._q1 + + @property + def q2(self) -> float: + """The q2 (j) value of the quaternion.""" + return self._q2 + + @property + def q3(self) -> float: + """The q3 (k) value of the quaternion.""" + return self._q3 + + @property + def angle_z(self) -> Angle: + """An Angle instance representing the z Euler component of the object's rotation. + + Defined as the rotation in the z axis. + """ + q0, q1, q2, q3 = self.q0_q1_q2_q3 + return Angle(radians=math.atan2(2 * (q1 * q2 + q0 * q3), 1 - 2 * (q2**2 + q3**2))) + + @property + def q0_q1_q2_q3(self): + """tuple of float: Contains all elements of the quaternion (q0,q1,q2,q3)""" + return self._q0, self._q1, self._q2, self._q3 + + def to_matrix(self, pos_x: float = 0.0, pos_y: float = 0.0, pos_z: float = 0.0): + """Convert the Quaternion to a 4x4 matrix representing this rotation. + + A position can also be provided to generate a full translation matrix. + + :param pos_x: The x component for the position. + :param pos_y: The y component for the position. + :param pos_z: The z component for the position. + + Returns: + :class:`anki_vector.util.Matrix44`: A matrix representing this Quaternion's + rotation, with the provided position (which defaults to 0,0,0). + """ + # See https://en.wikipedia.org/wiki/Quaternions_and_spatial_rotation + q0q0 = self.q0 * self.q0 + q1q1 = self.q1 * self.q1 + q2q2 = self.q2 * self.q2 + q3q3 = self.q3 * self.q3 + + q0x2 = self.q0 * 2.0 # saves 2 multiplies + q0q1x2 = q0x2 * self.q1 + q0q2x2 = q0x2 * self.q2 + q0q3x2 = q0x2 * self.q3 + q1x2 = self.q1 * 2.0 # saves 1 multiply + q1q2x2 = q1x2 * self.q2 + q1q3x2 = q1x2 * self.q3 + q2q3x2 = 2.0 * self.q2 * self.q3 + + m00 = (q0q0 + q1q1 - q2q2 - q3q3) + m01 = (q1q2x2 + q0q3x2) + m02 = (q1q3x2 - q0q2x2) + + m10 = (q1q2x2 - q0q3x2) + m11 = (q0q0 - q1q1 + q2q2 - q3q3) + m12 = (q0q1x2 + q2q3x2) + + m20 = (q0q2x2 + q1q3x2) + m21 = (q2q3x2 - q0q1x2) + m22 = (q0q0 - q1q1 - q2q2 + q3q3) + + return Matrix44(m00, m10, m20, float(pos_x), + m01, m11, m21, float(pos_y), + m02, m12, m22, float(pos_z), + 0.0, 0.0, 0.0, 1.0) + + def __repr__(self): + return (f"<{self.__class__.__name__} q0: {self.q0:.2f} q1: {self.q1:.2f}" + f" q2: {self.q2:.2f} q3: {self.q3:.2f} {self.angle_z}>") + + +class Position(Vector3): + """Represents the position of an object in the world. + + A position consists of its x, y and z values in millimeters. + + :param x: X position in millimeters + :param y: Y position in millimeters + :param z: Z position in millimeters + """ + __slots__ = () + + +class Pose: + """Represents where an object is in the world. + + Whenever Vector is delocalized (i.e. whenever Vector no longer knows + where he is - e.g. when he's picked up), Vector creates a new pose starting at + (0,0,0) with no rotation, with origin_id incremented to show that these poses + cannot be compared with earlier ones. As Vector drives around, his pose (and the + pose of other objects he observes - e.g. faces, his LightCube, charger, etc.) is relative to this + initial position and orientation. + + The coordinate space is relative to Vector, where Vector's origin is the + point on the ground between Vector's two front wheels. The X axis is Vector's forward direction, + the Y axis is to Vector's left, and the Z axis is up. + + Only poses of the same origin_id can safely be compared or operated on. + + .. testcode:: + + import anki_vector + from anki_vector.util import degrees, Pose + + with anki_vector.Robot() as robot: + pose = Pose(x=50, y=0, z=0, angle_z=anki_vector.util.Angle(degrees=0)) + robot.behavior.go_to_pose(pose) + """ + __slots__ = ('_position', '_rotation', '_origin_id') + + def __init__(self, x: float, y: float, z: float, q0: float = None, q1: float = None, q2: float = None, q3: float = None, + angle_z: Angle = None, origin_id: int = -1): + self._position = Position(x, y, z) + self._rotation = Quaternion(q0, q1, q2, q3, angle_z) + self._origin_id = origin_id + + @property + def position(self) -> Position: + """The position component of this pose.""" + return self._position + + @property + def rotation(self) -> Quaternion: + """The rotation component of this pose.""" + return self._rotation + + @property + def origin_id(self) -> int: + """An ID maintained by the robot which represents which coordinate frame this pose is in.""" + return self._origin_id + + def __repr__(self): + return (f"<{self.__class__.__name__}: {self._position}" + f" {self._rotation} >") + + def define_pose_relative_this(self, new_pose): + """Creates a new pose such that new_pose's origin is now at the location of this pose. + + :param anki_vector.util.Pose new_pose: The pose which origin is being changed. + + Returns: + A :class:`anki_vector.util.Pose` object for which the origin was this pose's origin. + """ + if not isinstance(new_pose, Pose): + raise TypeError("Unsupported type for new_origin, must be of type Pose") + x, y, z = self.position.x_y_z + angle_z = self.rotation.angle_z + new_x, new_y, new_z = new_pose.position.x_y_z + new_angle_z = new_pose.rotation.angle_z + + cos_angle = math.cos(angle_z.radians) + sin_angle = math.sin(angle_z.radians) + res_x = x + (cos_angle * new_x) - (sin_angle * new_y) + res_y = y + (sin_angle * new_x) + (cos_angle * new_y) + res_z = z + new_z + res_angle = angle_z + new_angle_z + return Pose(res_x, + res_y, + res_z, + angle_z=res_angle, + origin_id=self._origin_id) + + @property + def is_valid(self) -> bool: + """True if this is a valid, usable pose.""" + return self.origin_id >= 0 + + def is_comparable(self, other_pose) -> bool: + """Checks whether these two poses are comparable. + + Poses are comparable if they're valid and having matching origin IDs. + + :param other_pose: The other pose to compare against. Type is Pose. + + Returns: + True if the two poses are comparable, False otherwise. + """ + return (self.is_valid and other_pose.is_valid + and (self.origin_id == other_pose.origin_id)) + + def to_matrix(self) -> Matrix44: + """Convert the Pose to a Matrix44. + + Returns: + A matrix representing this Pose's position and rotation. + """ + return self.rotation.to_matrix(*self.position.x_y_z) + + def to_proto_pose_struct(self) -> protocol.PoseStruct: + """Converts the Pose into the robot's messaging pose format. + """ + return protocol.PoseStruct( + x=self._position.x, + y=self._position.y, + z=self._position.z, + q0=self._rotation.q0, + q1=self._rotation.q1, + q2=self._rotation.q2, + q3=self._rotation.q3, + origin_id=self._origin_id) + + +class ImageRect: + '''Defines a bounding box within an image frame. + + This is used when objects and faces are observed to denote where in + the robot's camera view the object or face actually appears. It's then + used by the annotate module to show an outline of a box around + the object or face. + ''' + + __slots__ = ('_x_top_left', '_y_top_left', '_width', '_height') + + def __init__(self, x_top_left: float, y_top_left: float, width: float, height: float): + self._x_top_left = float(x_top_left) + self._y_top_left = float(y_top_left) + self._width = float(width) + self._height = float(height) + + @property + def x_top_left(self) -> float: + """The top left x value of where the object was last visible within Vector's camera view.""" + return self._x_top_left + + @property + def y_top_left(self) -> float: + """The top left y value of where the object was last visible within Vector's camera view.""" + return self._y_top_left + + @property + def width(self) -> float: + """The width of the object from when it was last visible within Vector's camera view.""" + return self._width + + @property + def height(self) -> float: + """The height of the object from when it was last visible within Vector's camera view.""" + return self._height + + def scale_by(self, scale_multiplier: Union[int, float]) -> None: + """Scales the image rectangle by the multiplier provided.""" + if not isinstance(scale_multiplier, (int, float)): + raise TypeError("Unsupported operand for * expected number") + self._x_top_left *= scale_multiplier + self._y_top_left *= scale_multiplier + self._width *= scale_multiplier + self._height *= scale_multiplier + + +class Distance: + """Represents a distance. + + The class allows distances to be returned in either millimeters or inches. + + Use the :func:`distance_inches` or :func:`distance_mm` convenience methods to generate + a Distance instance. + + :param distance_mm: The number of millimeters the distance should + represent (cannot be combined with ``distance_inches``). + :param distance_inches: The number of inches the distance should + represent (cannot be combined with ``distance_mm``). + """ + + __slots__ = ('_distance_mm') + + def __init__(self, distance_mm: float = None, distance_inches: float = None): # pylint: disable=redefined-outer-name + if distance_mm is None and distance_inches is None: + raise ValueError("Expected either the distance_mm or distance_inches keyword argument") + if distance_mm and distance_inches: + raise ValueError("Expected either the distance_mm or distance_inches keyword argument, not both") + + if distance_inches is not None: + distance_mm = distance_inches * 25.4 + self._distance_mm = float(distance_mm) + + def __repr__(self): + return "<%s %.2f mm (%.2f inches)>" % (self.__class__.__name__, self.distance_mm, self.distance_inches) + + def __add__(self, other): + if not isinstance(other, Distance): + raise TypeError("Unsupported operand for + expected Distance") + return distance_mm(self.distance_mm + other.distance_mm) + + def __sub__(self, other): + if not isinstance(other, Distance): + raise TypeError("Unsupported operand for - expected Distance") + return distance_mm(self.distance_mm - other.distance_mm) + + def __mul__(self, other): + if not isinstance(other, (int, float)): + raise TypeError("Unsupported operand for * expected number") + return distance_mm(self.distance_mm * other) + + def __truediv__(self, other): + if not isinstance(other, (int, float)): + raise TypeError("Unsupported operand for / expected number") + return distance_mm(self.distance_mm / other) + + @property + def distance_mm(self) -> float: # pylint: disable=redefined-outer-name + """The distance in millimeters""" + return self._distance_mm + + @property + def distance_inches(self) -> float: # pylint: disable=redefined-outer-name + return self._distance_mm / 25.4 + + +def distance_mm(distance_mm: float): # pylint: disable=redefined-outer-name + """Returns an :class:`anki_vector.util.Distance` instance set to the specified number of millimeters.""" + return Distance(distance_mm=distance_mm) + + +def distance_inches(distance_inches: float): # pylint: disable=redefined-outer-name + """Returns an :class:`anki_vector.util.Distance` instance set to the specified number of inches.""" + return Distance(distance_inches=distance_inches) + + +class Speed: + """Represents a speed. + + This class allows speeds to be measured in millimeters per second. + + The maximum speed is 220 mm/s and is clamped internally. + + Use :func:`speed_mmps` convenience methods to generate + a Speed instance. + + :param speed_mmps: The number of millimeters per second the speed + should represent. + """ + + __slots__ = ('_speed_mmps') + + def __init__(self, speed_mmps: float = None): # pylint: disable=redefined-outer-name + if speed_mmps is None: + raise ValueError("Expected speed_mmps keyword argument") + self._speed_mmps = float(speed_mmps) + + def __repr__(self): + return "<%s %.2f mmps>" % (self.__class__.__name__, self.speed_mmps) + + def __add__(self, other): + if not isinstance(other, Speed): + raise TypeError("Unsupported operand for + expected Speed") + return speed_mmps(self.speed_mmps + other.speed_mmps) + + def __sub__(self, other): + if not isinstance(other, Speed): + raise TypeError("Unsupported operand for - expected Speed") + return speed_mmps(self.speed_mmps - other.speed_mmps) + + def __mul__(self, other): + if not isinstance(other, (int, float)): + raise TypeError("Unsupported operand for * expected number") + return speed_mmps(self.speed_mmps * other) + + def __truediv__(self, other): + if not isinstance(other, (int, float)): + raise TypeError("Unsupported operand for / expected number") + return speed_mmps(self.speed_mmps / other) + + @property + def speed_mmps(self: float) -> float: # pylint: disable=redefined-outer-name + """The speed in millimeters per second (mmps).""" + return self._speed_mmps + + +def speed_mmps(speed_mmps: float): # pylint: disable=redefined-outer-name + """:class:`anki_vector.util.Speed` instance set to the specified millimeters per second speed.""" + return Speed(speed_mmps=speed_mmps) + + +class BaseOverlay: + """A base overlay is used as a base class for other forms of overlays that can be drawn on top of an image. + + :param line_thickness: The thickness of the line being drawn. + :param line_color: The color of the line to be drawn. + """ + + def __init__(self, line_thickness: int, line_color: tuple): + self._line_thickness: int = line_thickness + self._line_color: tuple = line_color + + @property + def line_thickness(self) -> int: + """The thickness of the line being drawn.""" + return self._line_thickness + + @property + def line_color(self) -> tuple: + """The color of the line to be drawn.""" + return self._line_color + + +class RectangleOverlay(BaseOverlay): + """A rectangle that can be drawn on top of a given image. + + :param width: The width of the rectangle to be drawn. + :param height: The height of the rectangle to be drawn. + :param line_thickness: The thickness of the line being drawn. + :param line_color: The color of the line to be drawn. + """ + + # @TODO Implement overlay using an ImageRect rather than a raw width & height + def __init__(self, width: int, height: int, line_thickness: int = 5, line_color: tuple = (255, 0, 0)): + super().__init__(line_thickness, line_color) + self._width: int = width + self._height: int = height + + @property + def width(self) -> int: + """The width of the rectangle to be drawn.""" + return self._width + + @property + def height(self) -> int: + """The height of the rectangle to be drawn.""" + return self._height + + def apply_overlay(self, image: Image.Image) -> None: + """Draw a rectangle on top of the given image.""" + d = ImageDraw.Draw(image) + + image_width, image_height = image.size + remaining_width = image_width - self.width + remaining_height = image_height - self.height + x1, y1 = remaining_width // 2, remaining_height // 2 + x2, y2 = (image_width - (remaining_width // 2)), (image_height - (remaining_height // 2)) + + for i in range(0, self.line_thickness): + d.rectangle([x1 + i, y1 + i, x2 - i, y2 - i], outline=self.line_color) + + +class Component: + """ Base class for all components.""" + + def __init__(self, robot): + self.logger = get_class_logger(__name__, self) + self._robot = robot + + @property + def robot(self): + return self._robot + + @property + def conn(self): + return self._robot.conn + + @property + def force_async(self): + return self._robot.force_async + + @property + def grpc_interface(self): + """A direct reference to the connected aiogrpc interface. + """ + return self._robot.conn.grpc_interface + + +def read_configuration(serial: str, name: str, logger: logging.Logger, escape_pod:bool = False) -> dict: + """Open the default conf file, and read it into a :class:`configparser.ConfigParser` + If :code:`serial is not None`, this method will try to find a configuration with serial + number :code:`serial`, and raise an exception otherwise. If :code:`serial is None` and + :code:`name is not None`, this method will try to find a configuration which matches + the provided name, and raise an exception otherwise. If both :code:`serial is None` and + :code:`name is None`, this method will return a configuration if exactly `1` exists, but + if multiple configurations exists, it will raise an exception. + + :param serial: Vector's serial number + :param name: Vector's name + """ + home = Path.home() / ".anki_vector" + conf_file = str(home / "sdk_config.ini") + parser = configparser.ConfigParser(strict=False) + parser.read(conf_file) + + sections = parser.sections() + if not sections: + if escape_pod: + return {} + raise VectorConfigurationException('Could not find the sdk configuration file. Please run `python3 -m anki_vector.configure` or `python3 -m anki_vector.configure_pod` to set up your Vector for SDK usage.') + elif (serial is None) and (name is None): + if len(sections) == 1: + serial = sections[0] + logger.warning("No serial number or name provided. Automatically selecting {}".format(serial)) + else: + raise VectorConfigurationException("Found multiple robot serial numbers. " + "Please provide the serial number or name of the Robot you want to control.\n\n" + "Example: ./01_hello_world.py --serial {{robot_serial_number}}") + + config = {k.lower(): v for k, v in parser.items()} + + if serial is not None: + serial = serial.lower() + try: + return config[serial] + except KeyError: + raise VectorConfigurationException("Could not find matching robot info for given serial number: {}. " + "Please check your serial number is correct.\n\n" + "Example: ./01_hello_world.py --serial {{robot_serial_number}}", serial) + else: + for keySerial in config: + for key in config[keySerial]: + if config[keySerial][key] == name: + return config[keySerial] + if config[keySerial][key].lower() == name.lower(): + logger.warning("Using case-insensitive name match found in config. Set 'name' field to match 'Vector-A1B2' format.") + return config[keySerial] + + raise VectorConfigurationException("Could not find matching robot info for given name: {}. " + "Please check your name is correct.\n\n" + "Example: ./01_hello_world.py --name {{robot_name}}", name) diff --git a/anki_vector/version.py b/anki_vector/version.py index 63318c0..f9c10aa 100644 --- a/anki_vector/version.py +++ b/anki_vector/version.py @@ -1,15 +1,15 @@ -# Copyright (c) 2018 Anki, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License in the file LICENSE.txt or at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -__version__ = "0.7.2.dev0" +# Copyright (c) 2018 Anki, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License in the file LICENSE.txt or at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__version__ = "0.7.2.dev2" diff --git a/examples/tutorials/00_hello_escapepod.py b/examples/tutorials/00_hello_escapepod.py new file mode 100644 index 0000000..5465420 --- /dev/null +++ b/examples/tutorials/00_hello_escapepod.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2021 cyb3rdog +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License in the file LICENSE.txt or at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Hello Escape Pod + +Make Vector say 'Hello Escape Pod' in this simple Vector SDK example program. +""" + +import anki_vector + + +def main(): + with anki_vector.Robot(ip="192.168.0.148", escape_pod=True) as robot: + print("Say 'Hello Escape Pod'...") + robot.behavior.say_text("Hello Escape Pod") + + +if __name__ == "__main__": + main() diff --git a/setup.py b/setup.py index 9121cf6..745aad0 100644 --- a/setup.py +++ b/setup.py @@ -1,86 +1,88 @@ -# Copyright (c) 2018 Anki, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License in the file LICENSE.txt or at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -The Vector SDK gives you direct access to Vector's unprecedented set of advanced sensors, AI capabilities, and robotics technologies including computer vision, intelligent mapping and navigation, and a groundbreaking collection of expressive animations. - -It's powerful but easy to use, complex but not complicated, and versatile enough to be used across a wide range of domains including enterprise, research, and entertainment. Find out more at https://developer.anki.com - -Vector SDK documentation: https://developer.anki.com/vector/docs/ - -Official developer forum: https://forums.anki.com/ - -Requirements: - * Python 3.6.1 or later -""" - -import os.path -import sys -from setuptools import setup - -if sys.version_info < (3, 6, 1): - sys.exit('The Vector SDK requires Python 3.6.1 or later') - -HERE = os.path.abspath(os.path.dirname(__file__)) - -def fetch_version(): - """Get the version from the package""" - with open(os.path.join(HERE, 'anki_vector', 'version.py')) as version_file: - versions = {} - exec(version_file.read(), versions) - return versions - -VERSION_DATA = fetch_version() -VERSION = VERSION_DATA['__version__'] - -def get_requirements(): - """Load the requirements from requirements.txt into a list""" - reqs = [] - with open(os.path.join(HERE, 'requirements.txt')) as requirements_file: - for line in requirements_file: - reqs.append(line.strip()) - return reqs - -setup( - name='anki_vector', - version=VERSION, - description="The Vector SDK is a connected vision- and character-based robotics platform for everyone.", - long_description=__doc__, - url='https://developer.anki.com', - author='Anki, Inc', - author_email='developer@anki.com', - license='Apache License, Version 2.0', - # See https://pypi.python.org/pypi?%3Aaction=list_classifiers - classifiers=[ - 'Development Status :: 3 - Alpha', - 'Intended Audience :: Developers', - 'Topic :: Software Development :: Libraries', - 'License :: OSI Approved :: Apache Software License', - 'Programming Language :: Python :: 3.6', - ], - zip_safe=True, - keywords='anki vector robot robotics sdk ai vision'.split(), - packages=['anki_vector', 'anki_vector.camera_viewer', 'anki_vector.configure', 'anki_vector.messaging', 'anki_vector.opengl', 'anki_vector.reserve_control'], - package_data={ - 'anki_vector': ['LICENSE.txt', 'opengl/assets/*.obj', 'opengl/assets/*.mtl', 'opengl/assets/*.jpg', - 'opengl/assets/LICENSE.txt'] - }, - install_requires=get_requirements(), - extras_require={ - '3dviewer': ['PyOpenGL>=3.1'], - 'docs': ['sphinx', 'sphinx_rtd_theme', 'sphinx_autodoc_typehints'], - 'experimental': ['keras', 'scikit-learn', 'scipy', 'tensorflow'], - 'test': ['pytest', 'requests', 'requests_toolbelt'], - } -) +# Copyright (c) 2018 Anki, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License in the file LICENSE.txt or at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +The Vector SDK gives you direct access to Vector's unprecedented set of advanced sensors, AI capabilities, and robotics technologies including computer vision, intelligent mapping and navigation, and a groundbreaking collection of expressive animations. + +It's powerful but easy to use, complex but not complicated, and versatile enough to be used across a wide range of domains including enterprise, research, and entertainment. Find out more at https://developer.anki.com + +Vector SDK documentation: https://developer.anki.com/vector/docs/ + +Official developer forum: https://forums.anki.com/ + +Requirements: + * Python 3.6.1 or later +""" + +import os.path +import sys +from setuptools import setup + +if sys.version_info < (3, 6, 1): + sys.exit('The Vector SDK requires Python 3.6.1 or later') + +HERE = os.path.abspath(os.path.dirname(__file__)) + +def fetch_version(): + """Get the version from the package""" + with open(os.path.join(HERE, 'anki_vector', 'version.py')) as version_file: + versions = {} + exec(version_file.read(), versions) + return versions + +VERSION_DATA = fetch_version() +VERSION = VERSION_DATA['__version__'] + +def get_requirements(): + """Load the requirements from requirements.txt into a list""" + reqs = [] + with open(os.path.join(HERE, 'requirements.txt')) as requirements_file: + for line in requirements_file: + reqs.append(line.strip()) + return reqs + +setup( + name='cyb3r_vector_sdk', + version=VERSION, + description="The Vector SDK is a connected vision- and character-based robotics platform for everyone.", + long_description=__doc__, + url='https://github.com/cyb3rdog/vector-python-sdk', + author='Anki, Inc', + author_email='developer@anki.com', + license='Apache License, Version 2.0', + # See https://pypi.python.org/pypi?%3Aaction=list_classifiers + classifiers=[ + 'Development Status :: 3 - Alpha', + 'Intended Audience :: Developers', + 'Topic :: Software Development :: Libraries', + 'License :: OSI Approved :: Apache Software License', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', + ], + zip_safe=True, + keywords='anki vector robot robotics sdk ai vision'.split(), + packages=['anki_vector', 'anki_vector.camera_viewer', 'anki_vector.configure', 'anki_vector.configure_pod', 'anki_vector.messaging', 'anki_vector.opengl', 'anki_vector.reserve_control'], + package_data={ + 'anki_vector': ['LICENSE.txt', 'opengl/assets/*.obj', 'opengl/assets/*.mtl', 'opengl/assets/*.jpg', + 'opengl/assets/LICENSE.txt'] + }, + install_requires=get_requirements(), + extras_require={ + '3dviewer': ['PyOpenGL>=3.1'], + 'docs': ['sphinx', 'sphinx_rtd_theme', 'sphinx_autodoc_typehints'], + 'experimental': ['keras', 'scikit-learn', 'scipy', 'tensorflow'], + 'test': ['pytest', 'requests', 'requests_toolbelt'], + } +) From 5aa4ebb5152d38bb8aca8aa206ae333662a19e85 Mon Sep 17 00:00:00 2001 From: cyb3rdog Date: Wed, 14 Apr 2021 02:35:53 +0200 Subject: [PATCH 03/15] Update README.md --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 91914d7..52380eb 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ pip uninstall anki_vector pip uninstall ikkez_vector ``` -To install this SDK fork, run: +To install this SDK, run: ``` pip install cyb3r_vector_sdk ``` @@ -43,13 +43,13 @@ pip install cyb3r_vector_sdk --upgrade ### SDK Configuration -To condigure the SDK for **Prod**, or **Prod+OSKR** robot, run: +To configure the SDK for **Prod**, and/or **Prod+OSKR** robot, run: ``` py -m anki_vector.configure ``` -To condigure the SDK for **EscapePod**, or **EP+OSKR** robot, run: +To condigure the SDK for **EscapePod**, and/or **EP+OSKR** robot, run: ``` py -m anki_vector.configure_pod From fd47be2a8f26debc7a52c78ffd3ac5a4a5ec7ea0 Mon Sep 17 00:00:00 2001 From: cyb3rdog Date: Wed, 14 Apr 2021 08:19:24 +0200 Subject: [PATCH 04/15] first bugfix - escape pod non-ip login --- anki_vector/robot.py | 7 +++++-- anki_vector/version.py | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/anki_vector/robot.py b/anki_vector/robot.py index 530b830..f612ec5 100755 --- a/anki_vector/robot.py +++ b/anki_vector/robot.py @@ -128,7 +128,8 @@ def __init__(self, self.logger = util.get_class_logger(__name__, self) self._force_async = False config = config if config is not None else {} - config = {**util.read_configuration(serial, name, self.logger, escape_pod), **config} + if not escape_pod or ip is {}: + config = {**util.read_configuration(serial, name, self.logger, escape_pod), **config} if name is not None: vector_mdns = VectorMdns.find_vector(name) @@ -138,10 +139,12 @@ def __init__(self, self._escape_pod = escape_pod self._name = config["name"] if 'name' in config else None - self._ip = ip if ip is not {} else config["ip"] if 'ip' in config else None self._cert_file = config["cert"] if 'cert' in config else None self._guid = config["guid"] if 'guid' in config else None self._port = config["port"] if 'port' in config else "443" + self._ip = ip if ip is not {} else None + if self._ip is None and 'ip' in config: + self._ip = config["ip"] if (not escape_pod) and (self._name is None or self._ip is None or self._cert_file is None or self._guid is None): raise ValueError("The Robot object requires a serial and for Vector to be logged in (using the app then running the `python3 -m anki_vector.configure`).\n" diff --git a/anki_vector/version.py b/anki_vector/version.py index f9c10aa..62b4cfd 100644 --- a/anki_vector/version.py +++ b/anki_vector/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.7.2.dev2" +__version__ = "0.7.2.dev3" From 474a825783c5ae890b9e52834b3eca2a5269ccd3 Mon Sep 17 00:00:00 2001 From: cyb3rdog Date: Fri, 16 Apr 2021 02:22:06 +0200 Subject: [PATCH 05/15] Update README.md --- README.md | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 52380eb..e637f9a 100644 --- a/README.md +++ b/README.md @@ -4,21 +4,9 @@ ## With support for Production, EscapePod and OSKR robots! This is a fork of the original Anki Vector Python SDK. -I have started this fork as an unofficial version to keep things moving. ![Vector](docs/source/images/vector-sdk-alpha.jpg) -Learn more about Vector: https://www.anki.com/en-us/vector - -Learn more about how Vector works: [Vector Bible](https://github.com/GooeyChickenman/victor/blob/master/documentation/Vector-TRM.pdf) - -Learn more about the SDK: https://developer.anki.com/ - -SDK documentation: https://developer.anki.com/vector/docs/index.html - -Forums: https://forums.anki.com/ - - ## Getting Started You can follow steps [here](https://developer.anki.com/vector/docs/index.html) to set up your Vector robot with the SDK. @@ -49,7 +37,7 @@ To configure the SDK for **Prod**, and/or **Prod+OSKR** robot, run: py -m anki_vector.configure ``` -To condigure the SDK for **EscapePod**, and/or **EP+OSKR** robot, run: +To configure the SDK for **EscapePod**, and/or **EP+OSKR** robot, run: ``` py -m anki_vector.configure_pod @@ -69,6 +57,15 @@ You can either use the ```anki_vector.configure_pod``` in order to save your aut You can generate a local copy of the SDK documetation by following the instructions in the `docs` folder of this project. +Learn more about Vector: https://www.anki.com/en-us/vector + +Learn more about how Vector works: [Vector Bible](https://github.com/GooeyChickenman/victor/blob/master/documentation/Vector-TRM.pdf) + +Learn more about the SDK: https://developer.anki.com/ + +SDK documentation: https://developer.anki.com/vector/docs/index.html + +Forums: https://forums.anki.com/ ## Privacy Policy and Terms and Conditions From c6bfcad42961016aed845a5763ae4db01da7685d Mon Sep 17 00:00:00 2001 From: cyb3rdog Date: Tue, 27 Apr 2021 09:54:26 +0200 Subject: [PATCH 06/15] updated README.md --- README.md | 43 +++++++++++++++++++++++++++++-------------- 1 file changed, 29 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index e637f9a..546939a 100644 --- a/README.md +++ b/README.md @@ -7,51 +7,65 @@ This is a fork of the original Anki Vector Python SDK. ![Vector](docs/source/images/vector-sdk-alpha.jpg) + ## Getting Started You can follow steps [here](https://developer.anki.com/vector/docs/index.html) to set up your Vector robot with the SDK. + ### Installation In case you have previously installed the original anki or ikkez sdk, uninstall it with following commands: -``` +**``` pip uninstall anki_vector pip uninstall ikkez_vector -``` +```** To install this SDK, run: -``` +**``` pip install cyb3r_vector_sdk -``` +```** You can upgrade to latest version with: -``` +**``` pip install cyb3r_vector_sdk --upgrade -``` +```** + +In case you will run into dificulties during installation, run this command first: + +- Windows: ```py -m pip install -U pip``` +- Linux: ```python3 -m pip install -U pip``` + + +If you want to know where the SDK is installed use following command: + +- Windows: ```py -c "import anki_vector as _; print(_.__path__)"``` +- Linux: ```python3 -c "import anki_vector as _; print(_.__path__)"``` + ### SDK Configuration To configure the SDK for **Prod**, and/or **Prod+OSKR** robot, run: -``` -py -m anki_vector.configure -``` +- Windows: **```py -m anki_vector.configure```** +- Linux: **```python3 -m anki_vector.configure```** To configure the SDK for **EscapePod**, and/or **EP+OSKR** robot, run: -``` -py -m anki_vector.configure_pod -``` +- Windows: **```py -m anki_vector.configure_pod```** +- Linux: **```python3 -m anki_vector.configure_pod```** + -### SDK Usage - EscapePod +### SDK Usage - EscapePod -You can either use the ```anki_vector.configure_pod``` in order to save your authentication into the sdk_config.ini file, and use all the examples and your programs and as you have them, or you can use the Robot object with setting the escape_pod parameter to True, and passing the robot's ip address: +You can either use the ```anki_vector.configure_pod``` in order to save your authentication into the sdk_config.ini file, and use all the [examples](https://github.com/cyb3rdog/vector-python-sdk/tree/master/examples) and your own programs and as you have them, or you can use the Robot object with setting the escape_pod parameter to True, and passing the robot's ip address: ``` with anki_vector.Robot(ip="192.168.0.148", escape_pod=True) as robot: robot.behavior.say_text("Hello Escape Pod") ``` + ### Documentation You can generate a local copy of the SDK documetation by @@ -67,6 +81,7 @@ SDK documentation: https://developer.anki.com/vector/docs/index.html Forums: https://forums.anki.com/ + ## Privacy Policy and Terms and Conditions Use of Vector and the Vector SDK is subject to Anki's [Privacy Policy](https://www.anki.com/en-us/company/privacy) and [Terms and Conditions](https://www.anki.com/en-us/company/terms-and-conditions). From 34cb4352252ddcac3cf39c4cc090c80b57e8503e Mon Sep 17 00:00:00 2001 From: cyb3rdog Date: Tue, 27 Apr 2021 09:55:47 +0200 Subject: [PATCH 07/15] Update README.md --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 546939a..5f1d1a6 100644 --- a/README.md +++ b/README.md @@ -16,20 +16,20 @@ You can follow steps [here](https://developer.anki.com/vector/docs/index.html) t ### Installation In case you have previously installed the original anki or ikkez sdk, uninstall it with following commands: -**``` +``` pip uninstall anki_vector pip uninstall ikkez_vector -```** +``` To install this SDK, run: -**``` +``` pip install cyb3r_vector_sdk -```** +``` You can upgrade to latest version with: -**``` +``` pip install cyb3r_vector_sdk --upgrade -```** +``` In case you will run into dificulties during installation, run this command first: From c914700b829a9a03301cb3dd0c5d4c2d690be207 Mon Sep 17 00:00:00 2001 From: cyb3rdog Date: Tue, 27 Apr 2021 23:08:44 +0200 Subject: [PATCH 08/15] 0.7.2.dev4 - Added Timeout for ListAnimation error (DEADLINE_EXCEEDED) to 10s!! (likely bug in EscapePod firmware) - Added except handler for disconnecting robot (Python 3.8 fix) - Changed info of how to install 3dviewer - Updated README.md - Updated Examples --- README.md | 65 +- anki_vector/animation.py | 14 +- anki_vector/camera_viewer/__init__.py | 242 ++-- anki_vector/connection.py | 1684 +++++++++++----------- anki_vector/opengl/__init__.py | 4 +- anki_vector/opengl/opengl.py | 6 +- anki_vector/opengl/opengl_vector.py | 6 +- anki_vector/opengl/opengl_viewer.py | 4 +- anki_vector/version.py | 30 +- examples/tutorials/00_hello_escapepod.py | 64 +- examples/tutorials/10_play_audio.py | 4 +- 11 files changed, 1079 insertions(+), 1044 deletions(-) diff --git a/README.md b/README.md index 5f1d1a6..b02799e 100644 --- a/README.md +++ b/README.md @@ -1,43 +1,64 @@ # Anki/DDL Vector - Python SDK +by cyb3rdog ## With support for Production, EscapePod and OSKR robots! +Compatible with Ubuntu 16.04 - 20.04 and Python 3.6.1 - 3.9 -This is a fork of the original Anki Vector Python SDK. +This is the extended fork of the original Anki Vector Python SDK. ![Vector](docs/source/images/vector-sdk-alpha.jpg) ## Getting Started -You can follow steps [here](https://developer.anki.com/vector/docs/index.html) to set up your Vector robot with the SDK. +For the steps undocumented here, you can still refer to [this original SDK documentation](https://developer.anki.com/vector/docs/index.html). +*(TODO: docs are old and needs to be updated, contributors wanted)* +If you are new to the Vector's Python SDK, refer to this documentation anyways, as it still contains lots of valuable information. -### Installation +### Python Installation -In case you have previously installed the original anki or ikkez sdk, uninstall it with following commands: -``` -pip uninstall anki_vector -pip uninstall ikkez_vector -``` +#### Windows: -To install this SDK, run: -``` -pip install cyb3r_vector_sdk -``` +If you dont have python installed yet, download and install it from the [Python.org](https://www.python.org/downloads/windows/) +Be sure to tick the “Add Python 3.X to PATH” checkbox on the Setup screen. + +To avoid dificulties during the SDK install on your existing python installation, open the command line and run: +```py -m pip install -U pip``` +```py -m pip install --upgrade setuptools``` + +#### Linux: + +Open the Terminal and run following commands to install and update the Python, and packages required by SDK: +```sudo apt-get update``` +```sudo apt-get install -y python3 python3-pip python3-tk python3-pil.imagetk build-essential libssl-dev libffi-dev freeglut3``` + +```pip3 install --upgrade setuptools``` + + +### SDK Installation + +- Note: Use either **```pip```** or **```pip3```** correspondingly to the Python version you are using. + +In case you have previously installed the original ***Anki*** or ***Ikkez*** SDK, uninstall it/them with following command(s): + +```pip uninstall anki_vector``` or ```pip3 uninstall anki_vector``` +```pip uninstall ikkez_vector``` or ```pip3 uninstall ikkez_vector``` + +To install this new SDK, run: + +```pip install cyb3r_vector_sdk``` or ```pip3 install cyb3r_vector_sdk``` +and +```pip install --user "cyb3r_vector_sdk[3dviewer]"``` or ```pip3 install --user "cyb3r_vector_sdk[3dviewer]"``` -You can upgrade to latest version with: -``` -pip install cyb3r_vector_sdk --upgrade -``` -In case you will run into dificulties during installation, run this command first: +To upgrade this SDK to its latest version, use: -- Windows: ```py -m pip install -U pip``` -- Linux: ```python3 -m pip install -U pip``` +```pip install cyb3r_vector_sdk --upgrade``` or ```pip3 install cyb3r_vector_sdk --upgrade``` -If you want to know where the SDK is installed use following command: +If you want to know where the SDK files are installed, use following command: - Windows: ```py -c "import anki_vector as _; print(_.__path__)"``` - Linux: ```python3 -c "import anki_vector as _; print(_.__path__)"``` @@ -45,12 +66,12 @@ If you want to know where the SDK is installed use following command: ### SDK Configuration -To configure the SDK for **Prod**, and/or **Prod+OSKR** robot, run: +To configure the Python SDK for **Prod**, and/or **Prod+OSKR** robots, run: - Windows: **```py -m anki_vector.configure```** - Linux: **```python3 -m anki_vector.configure```** -To configure the SDK for **EscapePod**, and/or **EP+OSKR** robot, run: +To configure the Python SDK for **EscapePod**, and/or **EP+OSKR** robots, run: - Windows: **```py -m anki_vector.configure_pod```** - Linux: **```python3 -m anki_vector.configure_pod```** diff --git a/anki_vector/animation.py b/anki_vector/animation.py index 58490a3..80ac10d 100644 --- a/anki_vector/animation.py +++ b/anki_vector/animation.py @@ -131,14 +131,14 @@ async def _ensure_loaded(self): async def _load_animation_list(self): req = protocol.ListAnimationsRequest() - result = await self.grpc_interface.ListAnimations(req) + result = await self.grpc_interface.ListAnimations(req, timeout=10) self.logger.debug(f"Animation List status={text_format.MessageToString(result.status, as_one_line=True)}, number of animations={len(result.animation_names)}") self._anim_dict = {a.name: a for a in result.animation_names} return result async def _load_animation_trigger_list(self): req = protocol.ListAnimationTriggersRequest() - result = await self.grpc_interface.ListAnimationTriggers(req) + result = await self.grpc_interface.ListAnimationTriggers(req, timeout=10) self.logger.debug(f"Animation Triggers List status={text_format.MessageToString(result.status, as_one_line=True)}, number of animation_triggers={len(result.animation_trigger_names)}") self._anim_trigger_dict = {a.name: a for a in result.animation_trigger_names} return result @@ -165,7 +165,10 @@ async def load_animation_list(self): for anim_name in anim_names: print(anim_name) """ - return await self._load_animation_list() + try: + return await self._load_animation_list() + except: + return await self._load_animation_list() @connection.on_connection_thread(log_messaging=False, requires_control=False) async def load_animation_trigger_list(self): @@ -188,7 +191,10 @@ async def load_animation_trigger_list(self): for anim_trigger_name in anim_trigger_names: print(anim_trigger_name) """ - return await self._load_animation_trigger_list() + try: + return await self._load_animation_trigger_list() + except: + return await self._load_animation_trigger_list() # TODO: add return type hint @connection.on_connection_thread() diff --git a/anki_vector/camera_viewer/__init__.py b/anki_vector/camera_viewer/__init__.py index aacabcc..da66443 100644 --- a/anki_vector/camera_viewer/__init__.py +++ b/anki_vector/camera_viewer/__init__.py @@ -1,121 +1,121 @@ -# Copyright (c) 2018 Anki, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License in the file LICENSE.txt or at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""This module provides the camera viewer's render process. - -It should be launched in a separate process to allow Vector to run freely while -the viewer is rendering. - -It uses Tkinter, a standard Python GUI package. -It also depends on the Pillow library for image processing. -""" - -import multiprocessing as mp -import sys -import tkinter as tk - -try: - from PIL import ImageTk -except ImportError: - sys.exit("Cannot import from PIL: Do `pip3 install --user Pillow` to install") - - -class TkCameraViewer: # pylint: disable=too-few-public-methods - """A Tkinter based camera video feed. - - :param queue: A queue to send frames between the user's main thread and the viewer process. - :param event: An event to signal that the viewer process has closed. - :param overlays: Overlays to be drawn on the images of the renderer. - :param timeout: The time without a new frame before the process will exit. - :param force_on_top: Specifies whether the window should be forced on top of all others. - """ - - def __init__(self, queue: mp.Queue, event: mp.Event, overlays: list = None, timeout: float = 10.0, force_on_top: bool = True): - self.tk_root = tk.Tk() - self.width = None - self.height = None - self.queue = queue - self.event = event - self.overlays = overlays - self.timeout = timeout - self.tk_root.title("Vector Camera Feed") - self.tk_root.protocol("WM_DELETE_WINDOW", self._delete_window) - self.tk_root.bind("", self._resize_window) - if force_on_top: - self.tk_root.wm_attributes("-topmost", 1) - self.label = tk.Label(self.tk_root, borderwidth=0) - self.label.pack(fill=tk.BOTH, expand=True) - - def _delete_window(self) -> None: - """Handle window close event.""" - self.event.set() - self.tk_root.destroy() - - def _resize_window(self, evt: tk.Event) -> None: - """Handle window resize event. - - :param evt: A Tkinter window event (keyboard, mouse events, etc). - """ - self.width = evt.width - self.height = evt.height - - def draw_frame(self) -> None: - """Display an image on to a Tkinter label widget.""" - try: - image = self.queue.get(True, timeout=self.timeout) - except: - return - self.width, self.height = image.size - while image: - if self.event.is_set(): - break - if self.overlays: - for overlay in self.overlays: - overlay.apply_overlay(image) - if (self.width, self.height) != image.size: - image = image.resize((self.width, self.height)) - tk_image = ImageTk.PhotoImage(image) - self.label.config(image=tk_image) - self.label.image = tk_image - self.tk_root.update_idletasks() - self.tk_root.update() - try: - image = self.queue.get(True, timeout=self.timeout) - except: - return - - -def main(queue: mp.Queue, event: mp.Event, overlays: list = None, timeout: float = 10.0, force_on_top: bool = False) -> None: - """Rendering the frames in another process. This allows the UI to have the - main thread of its process while the user code continues to execute. - - :param queue: A queue to send frames between the user's main thread and the viewer process. - :param event: An event to signal that the viewer process has closed. - :param overlays: Overlays to be drawn on the images of the renderer. - :param timeout: The time without a new frame before the process will exit. - :param force_on_top: Specifies whether the window should be forced on top of all others. - """ - - try: - tk_viewer = TkCameraViewer(queue, event, overlays, timeout, force_on_top) - tk_viewer.draw_frame() - except TimeoutError: - pass - except KeyboardInterrupt: - pass - finally: - event.set() - - -__all__ = ['TkCameraViewer', 'main'] +# Copyright (c) 2018 Anki, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License in the file LICENSE.txt or at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""This module provides the camera viewer's render process. + +It should be launched in a separate process to allow Vector to run freely while +the viewer is rendering. + +It uses Tkinter, a standard Python GUI package. +It also depends on the Pillow library for image processing. +""" + +import multiprocessing as mp +import sys +import tkinter as tk + +try: + from PIL import ImageTk +except ImportError: + sys.exit('Cannot import from PIL: Do `pip3 install --user "cyb3r_vector_sdk[3dviewer]"` to install') + + +class TkCameraViewer: # pylint: disable=too-few-public-methods + """A Tkinter based camera video feed. + + :param queue: A queue to send frames between the user's main thread and the viewer process. + :param event: An event to signal that the viewer process has closed. + :param overlays: Overlays to be drawn on the images of the renderer. + :param timeout: The time without a new frame before the process will exit. + :param force_on_top: Specifies whether the window should be forced on top of all others. + """ + + def __init__(self, queue: mp.Queue, event: mp.Event, overlays: list = None, timeout: float = 10.0, force_on_top: bool = True): + self.tk_root = tk.Tk() + self.width = None + self.height = None + self.queue = queue + self.event = event + self.overlays = overlays + self.timeout = timeout + self.tk_root.title("Vector Camera Feed") + self.tk_root.protocol("WM_DELETE_WINDOW", self._delete_window) + self.tk_root.bind("", self._resize_window) + if force_on_top: + self.tk_root.wm_attributes("-topmost", 1) + self.label = tk.Label(self.tk_root, borderwidth=0) + self.label.pack(fill=tk.BOTH, expand=True) + + def _delete_window(self) -> None: + """Handle window close event.""" + self.event.set() + self.tk_root.destroy() + + def _resize_window(self, evt: tk.Event) -> None: + """Handle window resize event. + + :param evt: A Tkinter window event (keyboard, mouse events, etc). + """ + self.width = evt.width + self.height = evt.height + + def draw_frame(self) -> None: + """Display an image on to a Tkinter label widget.""" + try: + image = self.queue.get(True, timeout=self.timeout) + except: + return + self.width, self.height = image.size + while image: + if self.event.is_set(): + break + if self.overlays: + for overlay in self.overlays: + overlay.apply_overlay(image) + if (self.width, self.height) != image.size: + image = image.resize((self.width, self.height)) + tk_image = ImageTk.PhotoImage(image) + self.label.config(image=tk_image) + self.label.image = tk_image + self.tk_root.update_idletasks() + self.tk_root.update() + try: + image = self.queue.get(True, timeout=self.timeout) + except: + return + + +def main(queue: mp.Queue, event: mp.Event, overlays: list = None, timeout: float = 10.0, force_on_top: bool = False) -> None: + """Rendering the frames in another process. This allows the UI to have the + main thread of its process while the user code continues to execute. + + :param queue: A queue to send frames between the user's main thread and the viewer process. + :param event: An event to signal that the viewer process has closed. + :param overlays: Overlays to be drawn on the images of the renderer. + :param timeout: The time without a new frame before the process will exit. + :param force_on_top: Specifies whether the window should be forced on top of all others. + """ + + try: + tk_viewer = TkCameraViewer(queue, event, overlays, timeout, force_on_top) + tk_viewer.draw_frame() + except TimeoutError: + pass + except KeyboardInterrupt: + pass + finally: + event.set() + + +__all__ = ['TkCameraViewer', 'main'] diff --git a/anki_vector/connection.py b/anki_vector/connection.py index 9c04bc1..51c5cab 100644 --- a/anki_vector/connection.py +++ b/anki_vector/connection.py @@ -1,838 +1,846 @@ -# Copyright (c) 2018 Anki, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License in the file LICENSE.txt or at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -Management of the connection to and from Vector. -""" - -# __all__ should order by constants, event classes, other classes, functions. -__all__ = ['ControlPriorityLevel', 'Connection', 'on_connection_thread'] - -import asyncio -from concurrent import futures -from enum import Enum -import functools -import inspect -import logging -import platform -import sys -import threading -from typing import Any, Awaitable, Callable, Coroutine, Dict, List - -from google.protobuf.text_format import MessageToString -import grpc -import aiogrpc - -from . import util -from .escapepod import EscapePod -from .exceptions import (connection_error, - VectorAsyncException, - VectorBehaviorControlException, - VectorConfigurationException, - VectorControlException, - VectorControlTimeoutException, - VectorInvalidVersionException, - VectorNotFoundException) -from .messaging import client, protocol -from .version import __version__ - - -class CancelType(Enum): - """Enum used to specify cancellation options for behaviors -- internal use only """ - #: Cancellable as an 'Action' - CANCELLABLE_ACTION = 0 - #: Cancellable as a 'Behavior' - CANCELLABLE_BEHAVIOR = 1 - - -class ControlPriorityLevel(Enum): - """Enum used to specify the priority level for the program.""" - #: Runs above mandatory physical reactions, will drive off table, perform while on a slope, - #: ignore low battery state, work in the dark, etc. - OVERRIDE_BEHAVIORS_PRIORITY = protocol.ControlRequest.OVERRIDE_BEHAVIORS # pylint: disable=no-member - #: Runs below Mandatory Physical Reactions such as tucking Vector's head and arms during a fall, - #: yet above Trigger-Word Detection. Default for normal operation. - DEFAULT_PRIORITY = protocol.ControlRequest.DEFAULT # pylint: disable=no-member - #: Holds control of robot before/after other SDK connections - #: Used to disable idle behaviors. Not to be used for regular behavior control. - RESERVE_CONTROL = protocol.ControlRequest.RESERVE_CONTROL # pylint: disable=no-member - - -class _ControlEventManager: - """This manages every :class:`asyncio.Event` that handles the behavior control - system. - - These include three events: granted, lost, and request. - - :class:`granted_event` represents the behavior system handing control to the SDK. - - :class:`lost_event` represents a higher priority behavior taking control away from the SDK. - - :class:`request_event` Is a way of alerting :class:`Connection` to request control. - """ - - def __init__(self, loop: asyncio.BaseEventLoop = None, priority: ControlPriorityLevel = None): - self._granted_event = asyncio.Event(loop=loop) - self._lost_event = asyncio.Event(loop=loop) - self._request_event = asyncio.Event(loop=loop) - self._has_control = False - self._priority = priority - self._is_shutdown = False - - @property - def granted_event(self) -> asyncio.Event: - """This event is used to notify listeners that control has been granted to the SDK.""" - return self._granted_event - - @property - def lost_event(self) -> asyncio.Event: - """Represents a higher priority behavior taking control away from the SDK.""" - return self._lost_event - - @property - def request_event(self) -> asyncio.Event: - """Used to alert :class:`Connection` to request control.""" - return self._request_event - - @property - def has_control(self) -> bool: - """Check to see that the behavior system has control (without blocking by checking :class:`granted_event`)""" - return self._has_control - - @property - def priority(self) -> ControlPriorityLevel: - """The currently desired priority for the SDK.""" - return self._priority - - @property - def is_shutdown(self) -> bool: - """Detect if the behavior control stream is supposed to shut down.""" - return self._is_shutdown - - def request(self, priority: ControlPriorityLevel = ControlPriorityLevel.DEFAULT_PRIORITY) -> None: - """Tell the behavior stream to request control via setting the :class:`request_event`. - - This will signal Connection's :func:`_request_handler` generator to send a request control message on the BehaviorControl stream. - This signal happens asynchronously, and can be tracked using the :class:`granted_event` parameter. - - :param priority: The level of control in the behavior system. This determines which actions are allowed to - interrupt the SDK execution. See :class:`ControlPriorityLevel` for more information. - """ - if priority is None: - raise VectorBehaviorControlException("Must provide a priority level to request. To disable control, use {}.release().", self.__class__.__name__) - self._priority = priority - self._request_event.set() - - def release(self) -> None: - """Tell the behavior stream to release control via setting the :class:`request_event` while priority is ``None``. - - This will signal Connection's :func:`_request_handler` generator to send a release control message on the BehaviorControl stream. - This signal happens asynchronously, and can be tracked using the :class:`lost_event` parameter. - """ - self._priority = None - self._request_event.set() - - def update(self, enabled: bool) -> None: - """Update the current state of control (either enabled or disabled) - - :param enabled: Used to enable/disable behavior control - """ - self._has_control = enabled - if enabled: - self._granted_event.set() - self._lost_event.clear() - else: - self._lost_event.set() - self._granted_event.clear() - - def shutdown(self) -> None: - """Tells the control stream to shut down. - - This will return control to the rest of the behavior system. - """ - self._has_control = False - self._granted_event.set() - self._lost_event.set() - self._is_shutdown = True - self._request_event.set() - - -class Connection: - """Creates and maintains a aiogrpc connection including managing the connection thread. - The connection thread decouples the actual messaging layer from the user's main thread, - and requires any network requests to be ran using :func:`asyncio.run_coroutine_threadsafe` - to make them run on the other thread. Connection provides two helper functions for running - a function on the connection thread: :func:`~Connection.run_coroutine` and - :func:`~Connection.run_soon`. - - This class may be used to bypass the structures of the python sdk handled by - :class:`~anki_vector.robot.Robot`, and instead talk to aiogrpc more directly. - - The values for the cert_file location and the guid can be found in your home directory in - the sdk_config.ini file. - - .. code-block:: python - - import anki_vector - - # Connect to your Vector - conn = anki_vector.connection.Connection("Vector-XXXX", "XX.XX.XX.XX:443", "/path/to/file.cert", "") - conn.connect() - # Run your commands - async def play_animation(): - # Run your commands - anim = anki_vector.messaging.protocol.Animation(name="anim_pounce_success_02") - anim_request = anki_vector.messaging.protocol.PlayAnimationRequest(animation=anim) - return await conn.grpc_interface.PlayAnimation(anim_request) # This needs to be run in an asyncio loop - conn.run_coroutine(play_animation()).result() - # Close the connection - conn.close() - - :param name: Vector's name in the format of "Vector-XXXX". - :param host: The IP address and port of Vector in the format "XX.XX.XX.XX:443". - :param cert_file: The location of the certificate file on disk. - :param guid: Your robot's unique secret key. - :param behavior_control_level: pass one of :class:`ControlPriorityLevel` priority levels if the connection - requires behavior control, or None to decline control. - """ - - def __init__(self, name: str, host: str, cert_file: str, guid: str, escape_pod: bool = False, behavior_control_level: ControlPriorityLevel = ControlPriorityLevel.DEFAULT_PRIORITY): - self._loop: asyncio.BaseEventLoop = None - self.name = name - self.host = host - self.cert_file = cert_file - self._escape_pod = escape_pod - self._interface = None - self._channel = None - self._has_control = False - self._logger = util.get_class_logger(__name__, self) - self._control_stream_task = None - self._control_events: _ControlEventManager = None - self._guid = guid - self._thread: threading.Thread = None - self._ready_signal: threading.Event = threading.Event() - self._done_signal: asyncio.Event = None - self._conn_exception = False - self._behavior_control_level = behavior_control_level - self.active_commands = [] - - @property - def loop(self) -> asyncio.BaseEventLoop: - """A direct reference to the loop on the connection thread. - Can be used to run functions in on thread. - - .. testcode:: - - import anki_vector - import asyncio - - async def connection_function(): - print("I'm running in the connection thread event loop.") - - with anki_vector.Robot() as robot: - asyncio.run_coroutine_threadsafe(connection_function(), robot.conn.loop) - - :returns: The loop running inside the connection thread - """ - if self._loop is None: - raise VectorAsyncException("Attempted to access the connection loop before it was ready") - return self._loop - - @property - def thread(self) -> threading.Thread: - """A direct reference to the connection thread. Available to callers to determine if the - current thread is the connection thread. - - .. testcode:: - - import anki_vector - import threading - - with anki_vector.Robot() as robot: - if threading.current_thread() is robot.conn.thread: - print("This code is running on the connection thread") - else: - print("This code is not running on the connection thread") - - :returns: The connection thread where all of the grpc messages are being processed. - """ - if self._thread is None: - raise VectorAsyncException("Attempted to access the connection loop before it was ready") - return self._thread - - @property - def grpc_interface(self) -> client.ExternalInterfaceStub: - """A direct reference to the connected aiogrpc interface. - - This may be used to directly call grpc messages bypassing :class:`anki_vector.Robot` - - .. code-block:: python - - import anki_vector - - # Connect to your Vector - conn = anki_vector.connection.Connection("Vector-XXXX", "XX.XX.XX.XX:443", "/path/to/file.cert", "") - conn.connect() - # Run your commands - async def play_animation(): - # Run your commands - anim = anki_vector.messaging.protocol.Animation(name="anim_pounce_success_02") - anim_request = anki_vector.messaging.protocol.PlayAnimationRequest(animation=anim) - return await conn.grpc_interface.PlayAnimation(anim_request) # This needs to be run in an asyncio loop - conn.run_coroutine(play_animation()).result() - # Close the connection - conn.close() - """ - return self._interface - - @property - def behavior_control_level(self) -> ControlPriorityLevel: - """Returns the specific :class:`ControlPriorityLevel` requested for behavior control. - - To be able to directly control Vector's motors, override his screen, play an animation, etc., - the :class:`Connection` will need behavior control. This property identifies the enumerated - level of behavior control that the SDK will maintain over the robot. - - For more information about behavior control, see :ref:`behavior `. - - .. code-block:: python - - import anki_vector - - with anki_vector.Robot() as robot: - print(robot.conn.behavior_control_level) # Will print ControlPriorityLevel.DEFAULT_PRIORITY - robot.conn.release_control() - print(robot.conn.behavior_control_level) # Will print None - """ - return self._behavior_control_level - - @property - def requires_behavior_control(self) -> bool: - """True if the :class:`Connection` requires behavior control. - - To be able to directly control Vector's motors, override his screen, play an animation, etc., - the :class:`Connection` will need behavior control. This boolean signifies that - the :class:`Connection` will try to maintain control of Vector's behavior system even after losing - control to higher priority robot behaviors such as returning home to charge a low battery. - - For more information about behavior control, see :ref:`behavior `. - - .. code-block:: python - - import time - - import anki_vector - - def callback(robot, event_type, event): - robot.conn.request_control() - print(robot.conn.requires_behavior_control) # Will print True - robot.anim.play_animation_trigger('GreetAfterLongTime') - robot.conn.release_control() - - with anki_vector.Robot(behavior_control_level=None) as robot: - print(robot.conn.requires_behavior_control) # Will print False - robot.events.subscribe(callback, anki_vector.events.Events.robot_observed_face) - - # Waits 10 seconds. Show Vector your face. - time.sleep(10) - """ - return self._behavior_control_level is not None - - @property - def control_lost_event(self) -> asyncio.Event: - """This provides an :class:`asyncio.Event` that a user may :func:`wait()` upon to - detect when Vector has taken control of the behavior system at a higher priority. - - .. testcode:: - - import anki_vector - - async def auto_reconnect(conn: anki_vector.connection.Connection): - await conn.control_lost_event.wait() - conn.request_control() - """ - return self._control_events.lost_event - - @property - def control_granted_event(self) -> asyncio.Event: - """This provides an :class:`asyncio.Event` that a user may :func:`wait()` upon to - detect when Vector has given control of the behavior system to the SDK program. - - .. testcode:: - - import anki_vector - - async def wait_for_control(conn: anki_vector.connection.Connection): - await conn.control_granted_event.wait() - # Run commands that require behavior control - """ - return self._control_events.granted_event - - def request_control(self, behavior_control_level: ControlPriorityLevel = ControlPriorityLevel.DEFAULT_PRIORITY, timeout: float = 10.0): - """Explicitly request behavior control. Typically used after detecting :func:`control_lost_event`. - - To be able to directly control Vector's motors, override his screen, play an animation, etc., - the :class:`Connection` will need behavior control. This function will acquire control - of Vector's behavior system. This will raise a :class:`VectorControlTimeoutException` if it fails - to gain control before the timeout. - - For more information about behavior control, see :ref:`behavior ` - - .. testcode:: - - import anki_vector - - async def auto_reconnect(conn: anki_vector.connection.Connection): - await conn.control_lost_event.wait() - conn.request_control(timeout=5.0) - - :param timeout: The time allotted to attempt a connection, in seconds. - :param behavior_control_level: request control of Vector's behavior system at a specific level of control. - See :class:`ControlPriorityLevel` for more information. - """ - if not isinstance(behavior_control_level, ControlPriorityLevel): - raise TypeError("behavior_control_level must be of type ControlPriorityLevel") - if self._thread is threading.current_thread(): - return asyncio.ensure_future(self._request_control(behavior_control_level=behavior_control_level, timeout=timeout), loop=self._loop) - return self.run_coroutine(self._request_control(behavior_control_level=behavior_control_level, timeout=timeout)) - - async def _request_control(self, behavior_control_level: ControlPriorityLevel = ControlPriorityLevel.DEFAULT_PRIORITY, timeout: float = 10.0): - self._behavior_control_level = behavior_control_level - self._control_events.request(self._behavior_control_level) - try: - self._has_control = await asyncio.wait_for(self.control_granted_event.wait(), timeout) - except futures.TimeoutError as e: - raise VectorControlTimeoutException(f"Surpassed timeout of {timeout}s") from e - - def release_control(self, timeout: float = 10.0): - """Explicitly release control. Typically used after detecting :func:`control_lost_event`. - - To be able to directly control Vector's motors, override his screen, play an animation, etc., - the :class:`Connection` will need behavior control. This function will release control - of Vector's behavior system. This will raise a :class:`VectorControlTimeoutException` if it fails - to receive a control_lost event before the timeout. - - .. testcode:: - - import anki_vector - - async def wait_for_control(conn: anki_vector.connection.Connection): - await conn.control_granted_event.wait() - # Run commands that require behavior control - conn.release_control() - - :param timeout: The time allotted to attempt to release control, in seconds. - """ - if self._thread is threading.current_thread(): - return asyncio.ensure_future(self._release_control(timeout=timeout), loop=self._loop) - return self.run_coroutine(self._release_control(timeout=timeout)) - - async def _release_control(self, timeout: float = 10.0): - self._behavior_control_level = None - self._control_events.release() - try: - self._has_control = await asyncio.wait_for(self.control_lost_event.wait(), timeout) - except futures.TimeoutError as e: - raise VectorControlTimeoutException(f"Surpassed timeout of {timeout}s") from e - - def connect(self, timeout: float = 10.0) -> None: - """Connect to Vector. This will start the connection thread which handles all messages - between Vector and Python. - - .. code-block:: python - - import anki_vector - - # Connect to your Vector - conn = anki_vector.connection.Connection("Vector-XXXX", "XX.XX.XX.XX:443", "/path/to/file.cert", "") - conn.connect() - # Run your commands - async def play_animation(): - # Run your commands - anim = anki_vector.messaging.protocol.Animation(name="anim_pounce_success_02") - anim_request = anki_vector.messaging.protocol.PlayAnimationRequest(animation=anim) - return await conn.grpc_interface.PlayAnimation(anim_request) # This needs to be run in an asyncio loop - conn.run_coroutine(play_animation()).result() - # Close the connection - conn.close() - - :param timeout: The time allotted to attempt a connection, in seconds. - """ - if self._thread: - raise VectorAsyncException("\n\nRepeated connections made to open Connection.") - self._ready_signal.clear() - self._thread = threading.Thread(target=self._connect, args=(timeout,), daemon=True, name="gRPC Connection Handler Thread") - self._thread.start() - ready = self._ready_signal.wait(timeout=4 * timeout) - if not ready: - raise VectorNotFoundException() - if hasattr(self._ready_signal, "exception"): - e = getattr(self._ready_signal, "exception") - delattr(self._ready_signal, "exception") - raise e - - def _connect(self, timeout: float) -> None: - """The function that runs on the connection thread. This will connect to Vector, - and establish the BehaviorControl stream. - """ - try: - if threading.main_thread() is threading.current_thread(): - raise VectorAsyncException("\n\nConnection._connect must be run outside of the main thread.") - self._loop = asyncio.new_event_loop() - asyncio.set_event_loop(self._loop) - self._done_signal = asyncio.Event() - if not self._behavior_control_level: - self._control_events = _ControlEventManager(self._loop) - else: - self._control_events = _ControlEventManager(self._loop, priority=self._behavior_control_level) - - trusted_certs = None - if not self.cert_file is None: - with open(self.cert_file, 'rb') as cert: - trusted_certs = cert.read() - else: - if not self._escape_pod: - raise VectorConfigurationException("Must provide a cert file to authenticate to Vector.") - - if self._escape_pod: - if not EscapePod.validate_certificate_name(self.cert_file, self.name): - trusted_certs = EscapePod.get_authentication_certificate(self.host) - self.name = EscapePod.get_certificate_name(trusted_certs) - self._guid = EscapePod.authenticate_escape_pod(self.host, self.name, trusted_certs) - - # Pin the robot certificate for opening the channel - channel_credentials = aiogrpc.ssl_channel_credentials(root_certificates=trusted_certs) - # Add authorization header for all the calls - call_credentials = aiogrpc.access_token_call_credentials(self._guid) - - credentials = aiogrpc.composite_channel_credentials(channel_credentials, call_credentials) - - self._logger.info(f"Connecting to {self.host} for {self.name} using {self.cert_file}") - self._channel = aiogrpc.secure_channel(self.host, credentials, - options=(("grpc.ssl_target_name_override", self.name,),)) - - # Verify the connection to Vector is able to be established (client-side) - try: - # Explicitly grab _channel._channel to test the underlying grpc channel directly - grpc.channel_ready_future(self._channel._channel).result(timeout=timeout) # pylint: disable=protected-access - except grpc.FutureTimeoutError as e: - raise VectorNotFoundException() from e - - self._interface = client.ExternalInterfaceStub(self._channel) - - # Verify Vector and the SDK have compatible protocol versions - version = protocol.ProtocolVersionRequest(client_version=protocol.PROTOCOL_VERSION_CURRENT, min_host_version=protocol.PROTOCOL_VERSION_MINIMUM) - protocol_version = self._loop.run_until_complete(self._interface.ProtocolVersion(version)) - if protocol_version.result != protocol.ProtocolVersionResponse.SUCCESS or protocol.PROTOCOL_VERSION_MINIMUM > protocol_version.host_version: # pylint: disable=no-member - raise VectorInvalidVersionException(protocol_version) - - self._control_stream_task = self._loop.create_task(self._open_connections()) - - # Initialze SDK - sdk_module_version = __version__ - python_version = platform.python_version() - python_implementation = platform.python_implementation() - os_version = platform.platform() - cpu_version = platform.machine() - initialize = protocol.SDKInitializationRequest(sdk_module_version=sdk_module_version, - python_version=python_version, - python_implementation=python_implementation, - os_version=os_version, - cpu_version=cpu_version) - self._loop.run_until_complete(self._interface.SDKInitialization(initialize)) - - if self._behavior_control_level: - self._loop.run_until_complete(self._request_control(behavior_control_level=self._behavior_control_level, timeout=timeout)) - except Exception as e: # pylint: disable=broad-except - # Propagate the errors to the calling thread - setattr(self._ready_signal, "exception", e) - self._loop.close() - return - finally: - self._ready_signal.set() - - try: - async def wait_until_done(): - return await self._done_signal.wait() - self._loop.run_until_complete(wait_until_done()) - finally: - self._loop.close() - - async def _request_handler(self): - """Handles generating messages for the BehaviorControl stream.""" - while await self._control_events.request_event.wait(): - self._control_events.request_event.clear() - if self._control_events.is_shutdown: - return - priority = self._control_events.priority - if priority is None: - msg = protocol.ControlRelease() - msg = protocol.BehaviorControlRequest(control_release=msg) - else: - msg = protocol.ControlRequest(priority=priority.value) - msg = protocol.BehaviorControlRequest(control_request=msg) - self._logger.debug(f"BehaviorControl {MessageToString(msg, as_one_line=True)}") - yield msg - - async def _open_connections(self): - """Starts the BehaviorControl stream, and handles the messages coming back from the robot.""" - try: - async for response in self._interface.BehaviorControl(self._request_handler()): - response_type = response.WhichOneof("response_type") - if response_type == 'control_granted_response': - self._logger.info(f"BehaviorControl {MessageToString(response, as_one_line=True)}") - self._control_events.update(True) - elif response_type == 'control_lost_event': - self._cancel_active() - self._logger.info(f"BehaviorControl {MessageToString(response, as_one_line=True)}") - self._control_events.update(False) - except futures.CancelledError: - self._logger.debug('Behavior handler task was cancelled. This is expected during disconnection.') - - def _cancel_active(self): - for fut in self.active_commands: - if not fut.done(): - fut.cancel() - self.active_commands = [] - - def close(self): - """Cleanup the connection, and shutdown all the event handlers. - - Usually this should be invoked by the Robot class when it closes. - - .. code-block:: python - - import anki_vector - - # Connect to your Vector - conn = anki_vector.connection.Connection("Vector-XXXX", "XX.XX.XX.XX:443", "/path/to/file.cert", "") - conn.connect() - # Run your commands - async def play_animation(): - # Run your commands - anim = anki_vector.messaging.protocol.Animation(name="anim_pounce_success_02") - anim_request = anki_vector.messaging.protocol.PlayAnimationRequest(animation=anim) - return await conn.grpc_interface.PlayAnimation(anim_request) # This needs to be run in an asyncio loop - conn.run_coroutine(play_animation()).result() - # Close the connection - conn.close() - """ - if self._control_events: - self._control_events.shutdown() - if self._control_stream_task: - self._control_stream_task.cancel() - self.run_coroutine(self._control_stream_task).result() - self._cancel_active() - if self._channel: - self.run_coroutine(self._channel.close()).result() - self.run_coroutine(self._done_signal.set) - self._thread.join(timeout=5) - self._thread = None - - def run_soon(self, coro: Awaitable) -> None: - """Schedules the given awaitable to run on the event loop for the connection thread. - - .. testcode:: - - import anki_vector - import time - - async def my_coroutine(): - print("Running on the connection thread") - - with anki_vector.Robot() as robot: - robot.conn.run_soon(my_coroutine()) - time.sleep(1) - - :param coro: The coroutine, task or any awaitable to schedule for execution on the connection thread. - """ - if coro is None or not inspect.isawaitable(coro): - raise VectorAsyncException(f"\n\n{coro.__name__ if hasattr(coro, '__name__') else coro} is not awaitable, so cannot be ran with run_soon.\n") - - def soon(): - try: - asyncio.ensure_future(coro) - except TypeError as e: - raise VectorAsyncException(f"\n\n{coro.__name__ if hasattr(coro, '__name__') else coro} could not be ensured as a future.\n") from e - if threading.current_thread() is self._thread: - self._loop.call_soon(soon) - else: - self._loop.call_soon_threadsafe(soon) - - def run_coroutine(self, coro: Awaitable) -> Any: - """Runs a given awaitable on the connection thread's event loop. - Cannot be called from within the connection thread. - - .. testcode:: - - import anki_vector - - async def my_coroutine(): - print("Running on the connection thread") - return "Finished" - - with anki_vector.Robot() as robot: - result = robot.conn.run_coroutine(my_coroutine()) - - :param coro: The coroutine, task or any other awaitable which should be executed. - :returns: The result of the awaitable's execution. - """ - if threading.current_thread() is self._thread: - raise VectorAsyncException("Attempting to invoke async from same thread." - "Instead you may want to use 'run_soon'") - if asyncio.iscoroutinefunction(coro) or asyncio.iscoroutine(coro): - return self._run_coroutine(coro) - if asyncio.isfuture(coro): - async def future_coro(): - return await coro - return self._run_coroutine(future_coro()) - if callable(coro): - async def wrapped_coro(): - return coro() - return self._run_coroutine(wrapped_coro()) - raise VectorAsyncException("\n\nInvalid parameter to run_coroutine: {}\n" - "This function expects a coroutine, task, or awaitable.".format(type(coro))) - - def _run_coroutine(self, coro): - return asyncio.run_coroutine_threadsafe(coro, self._loop) - - -def on_connection_thread(log_messaging: bool = True, requires_control: bool = True, is_cancellable: CancelType = None) -> Callable[[Coroutine[util.Component, Any, None]], Any]: - """A decorator generator used internally to denote which functions will run on - the connection thread. This unblocks the caller of the wrapped function - and allows them to continue running while the messages are being processed. - - .. code-block:: python - - import anki_vector - - class MyComponent(anki_vector.util.Component): - @connection._on_connection_thread() - async def on_connection_thread(self): - # Do work on the connection thread - - :param log_messaging: True if the log output should include the entire message or just the size. Recommended for - large binary return values. - :param requires_control: True if the function should wait until behavior control is granted before executing. - :param is_cancellable: use a valid enum of :class:`CancelType` to specify the type of cancellation for the - function. Defaults to 'None' implying no support for responding to cancellation. - :returns: A decorator which has 3 possible returns based on context: the result of the decorated function, - the :class:`concurrent.futures.Future` which points to the decorated function, or the - :class:`asyncio.Future` which points to the decorated function. - These contexts are: when the robot is a :class:`~anki_vector.robot.Robot`, - when the robot is an :class:`~anki_vector.robot.AsyncRobot`, and when - called from the connection thread respectively. - """ - def _on_connection_thread_decorator(func: Coroutine) -> Any: - """A decorator which specifies a function to be executed on the connection thread - - :params func: The function to be decorated - :returns: There are 3 possible returns based on context: the result of the decorated function, - the :class:`concurrent.futures.Future` which points to the decorated function, or the - :class:`asyncio.Future` which points to the decorated function. - These contexts are: when the robot is a :class:`anki_vector.robot.Robot`, - when the robot is an :class:`anki_vector.robot.AsyncRobot`, and when - called from the connection thread respectively. - """ - if not asyncio.iscoroutinefunction(func): - raise VectorAsyncException("\n\nCannot define non-coroutine function '{}' to run on connection thread.\n" - "Make sure the function is defined using 'async def'.".format(func.__name__ if hasattr(func, "__name__") else func)) - - @functools.wraps(func) - async def log_handler(conn: Connection, func: Coroutine, logger: logging.Logger, *args: List[Any], **kwargs: Dict[str, Any]) -> Coroutine: - """Wrap the provided coroutine to better express exceptions as specific :class:`anki_vector.exceptions.VectorException`s, and - adds logging to incoming (from the robot) and outgoing (to the robot) messages. - """ - result = None - # TODO: only have the request wait for control if we're not done. If done raise an exception. - control = conn.control_granted_event - if requires_control and not control.is_set(): - if not conn.requires_behavior_control: - raise VectorControlException(func.__name__) - logger.info(f"Delaying {func.__name__} until behavior control is granted") - await asyncio.wait([conn.control_granted_event.wait()], timeout=10) - message = args[1:] - outgoing = message if log_messaging else "size = {} bytes".format(sys.getsizeof(message)) - logger.debug(f'Outgoing {func.__name__}: {outgoing}') - try: - result = await func(*args, **kwargs) - except grpc.RpcError as rpc_error: - raise connection_error(rpc_error) from rpc_error - incoming = str(result).strip() if log_messaging else "size = {} bytes".format(sys.getsizeof(result)) - logger.debug(f'Incoming {func.__name__}: {type(result).__name__} {incoming}') - return result - - @functools.wraps(func) - def result(*args: List[Any], **kwargs: Dict[str, Any]) -> Any: - """The function that is the result of the decorator. Provides a wrapped function. - - :param _return_future: A hidden parameter which allows the wrapped function to explicitly - return a future (default for AsyncRobot) or not (default for Robot). - :returns: Based on context this can return the result of the decorated function, - the :class:`concurrent.futures.Future` which points to the decorated function, or the - :class:`asyncio.Future` which points to the decorated function. - These contexts are: when the robot is a :class:`anki_vector.robot.Robot`, - when the robot is an :class:`anki_vector.robot.AsyncRobot`, and when - called from the connection thread respectively.""" - self = args[0] # Get the self reference from the function call - # if the call supplies a _return_future parameter then override force_async with that. - _return_future = kwargs.pop('_return_future', self.force_async) - - action_id = None - if is_cancellable == CancelType.CANCELLABLE_ACTION: - action_id = self._get_next_action_id() - kwargs['_action_id'] = action_id - - wrapped_coroutine = log_handler(self.conn, func, self.logger, *args, **kwargs) - - if threading.current_thread() == self.conn.thread: - if self.conn.loop.is_running(): - return asyncio.ensure_future(wrapped_coroutine, loop=self.conn.loop) - raise VectorAsyncException("\n\nThe connection thread loop is not running, but a " - "function '{}' is being invoked on that thread.\n".format(func.__name__ if hasattr(func, "__name__") else func)) - future = asyncio.run_coroutine_threadsafe(wrapped_coroutine, self.conn.loop) - - if is_cancellable == CancelType.CANCELLABLE_ACTION: - def user_cancelled_action(fut): - if action_id is None: - return - - if fut.cancelled(): - self._abort_action(action_id) - - future.add_done_callback(user_cancelled_action) - - if is_cancellable == CancelType.CANCELLABLE_BEHAVIOR: - def user_cancelled_behavior(fut): - if fut.cancelled(): - self._abort_behavior() - - future.add_done_callback(user_cancelled_behavior) - - if requires_control: - self.conn.active_commands.append(future) - - def clear_when_done(fut): - if fut in self.conn.active_commands: - self.conn.active_commands.remove(fut) - future.add_done_callback(clear_when_done) - if _return_future: - return future - try: - return future.result() - except futures.CancelledError: - self.logger.warning(f"{func.__name__} cancelled because behavior control was lost") - return None - return result - return _on_connection_thread_decorator +# Copyright (c) 2018 Anki, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License in the file LICENSE.txt or at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Management of the connection to and from Vector. +""" + +# __all__ should order by constants, event classes, other classes, functions. +__all__ = ['ControlPriorityLevel', 'Connection', 'on_connection_thread'] + +import asyncio +from concurrent import futures +from enum import Enum +import functools +import inspect +import logging +import platform +import sys +import threading +from typing import Any, Awaitable, Callable, Coroutine, Dict, List + +from google.protobuf.text_format import MessageToString +import grpc +import aiogrpc + +from . import util +from .escapepod import EscapePod +from .exceptions import (connection_error, + VectorAsyncException, + VectorBehaviorControlException, + VectorConfigurationException, + VectorControlException, + VectorControlTimeoutException, + VectorInvalidVersionException, + VectorNotFoundException) +from .messaging import client, protocol +from .version import __version__ + + +class CancelType(Enum): + """Enum used to specify cancellation options for behaviors -- internal use only """ + #: Cancellable as an 'Action' + CANCELLABLE_ACTION = 0 + #: Cancellable as a 'Behavior' + CANCELLABLE_BEHAVIOR = 1 + + +class ControlPriorityLevel(Enum): + """Enum used to specify the priority level for the program.""" + #: Runs above mandatory physical reactions, will drive off table, perform while on a slope, + #: ignore low battery state, work in the dark, etc. + OVERRIDE_BEHAVIORS_PRIORITY = protocol.ControlRequest.OVERRIDE_BEHAVIORS # pylint: disable=no-member + #: Runs below Mandatory Physical Reactions such as tucking Vector's head and arms during a fall, + #: yet above Trigger-Word Detection. Default for normal operation. + DEFAULT_PRIORITY = protocol.ControlRequest.DEFAULT # pylint: disable=no-member + #: Holds control of robot before/after other SDK connections + #: Used to disable idle behaviors. Not to be used for regular behavior control. + RESERVE_CONTROL = protocol.ControlRequest.RESERVE_CONTROL # pylint: disable=no-member + + +class _ControlEventManager: + """This manages every :class:`asyncio.Event` that handles the behavior control + system. + + These include three events: granted, lost, and request. + + :class:`granted_event` represents the behavior system handing control to the SDK. + + :class:`lost_event` represents a higher priority behavior taking control away from the SDK. + + :class:`request_event` Is a way of alerting :class:`Connection` to request control. + """ + + def __init__(self, loop: asyncio.BaseEventLoop = None, priority: ControlPriorityLevel = None): + self._granted_event = asyncio.Event(loop=loop) + self._lost_event = asyncio.Event(loop=loop) + self._request_event = asyncio.Event(loop=loop) + self._has_control = False + self._priority = priority + self._is_shutdown = False + + @property + def granted_event(self) -> asyncio.Event: + """This event is used to notify listeners that control has been granted to the SDK.""" + return self._granted_event + + @property + def lost_event(self) -> asyncio.Event: + """Represents a higher priority behavior taking control away from the SDK.""" + return self._lost_event + + @property + def request_event(self) -> asyncio.Event: + """Used to alert :class:`Connection` to request control.""" + return self._request_event + + @property + def has_control(self) -> bool: + """Check to see that the behavior system has control (without blocking by checking :class:`granted_event`)""" + return self._has_control + + @property + def priority(self) -> ControlPriorityLevel: + """The currently desired priority for the SDK.""" + return self._priority + + @property + def is_shutdown(self) -> bool: + """Detect if the behavior control stream is supposed to shut down.""" + return self._is_shutdown + + def request(self, priority: ControlPriorityLevel = ControlPriorityLevel.DEFAULT_PRIORITY) -> None: + """Tell the behavior stream to request control via setting the :class:`request_event`. + + This will signal Connection's :func:`_request_handler` generator to send a request control message on the BehaviorControl stream. + This signal happens asynchronously, and can be tracked using the :class:`granted_event` parameter. + + :param priority: The level of control in the behavior system. This determines which actions are allowed to + interrupt the SDK execution. See :class:`ControlPriorityLevel` for more information. + """ + if priority is None: + raise VectorBehaviorControlException("Must provide a priority level to request. To disable control, use {}.release().", self.__class__.__name__) + self._priority = priority + self._request_event.set() + + def release(self) -> None: + """Tell the behavior stream to release control via setting the :class:`request_event` while priority is ``None``. + + This will signal Connection's :func:`_request_handler` generator to send a release control message on the BehaviorControl stream. + This signal happens asynchronously, and can be tracked using the :class:`lost_event` parameter. + """ + self._priority = None + self._request_event.set() + + def update(self, enabled: bool) -> None: + """Update the current state of control (either enabled or disabled) + + :param enabled: Used to enable/disable behavior control + """ + self._has_control = enabled + if enabled: + self._granted_event.set() + self._lost_event.clear() + else: + self._lost_event.set() + self._granted_event.clear() + + def shutdown(self) -> None: + """Tells the control stream to shut down. + + This will return control to the rest of the behavior system. + """ + self._has_control = False + self._granted_event.set() + self._lost_event.set() + self._is_shutdown = True + self._request_event.set() + + +class Connection: + """Creates and maintains a aiogrpc connection including managing the connection thread. + The connection thread decouples the actual messaging layer from the user's main thread, + and requires any network requests to be ran using :func:`asyncio.run_coroutine_threadsafe` + to make them run on the other thread. Connection provides two helper functions for running + a function on the connection thread: :func:`~Connection.run_coroutine` and + :func:`~Connection.run_soon`. + + This class may be used to bypass the structures of the python sdk handled by + :class:`~anki_vector.robot.Robot`, and instead talk to aiogrpc more directly. + + The values for the cert_file location and the guid can be found in your home directory in + the sdk_config.ini file. + + .. code-block:: python + + import anki_vector + + # Connect to your Vector + conn = anki_vector.connection.Connection("Vector-XXXX", "XX.XX.XX.XX:443", "/path/to/file.cert", "") + conn.connect() + # Run your commands + async def play_animation(): + # Run your commands + anim = anki_vector.messaging.protocol.Animation(name="anim_pounce_success_02") + anim_request = anki_vector.messaging.protocol.PlayAnimationRequest(animation=anim) + return await conn.grpc_interface.PlayAnimation(anim_request) # This needs to be run in an asyncio loop + conn.run_coroutine(play_animation()).result() + # Close the connection + conn.close() + + :param name: Vector's name in the format of "Vector-XXXX". + :param host: The IP address and port of Vector in the format "XX.XX.XX.XX:443". + :param cert_file: The location of the certificate file on disk. + :param guid: Your robot's unique secret key. + :param behavior_control_level: pass one of :class:`ControlPriorityLevel` priority levels if the connection + requires behavior control, or None to decline control. + """ + + def __init__(self, name: str, host: str, cert_file: str, guid: str, escape_pod: bool = False, behavior_control_level: ControlPriorityLevel = ControlPriorityLevel.DEFAULT_PRIORITY): + self._loop: asyncio.BaseEventLoop = None + self.name = name + self.host = host + self.cert_file = cert_file + self._escape_pod = escape_pod + self._interface = None + self._channel = None + self._has_control = False + self._logger = util.get_class_logger(__name__, self) + self._control_stream_task = None + self._control_events: _ControlEventManager = None + self._guid = guid + self._thread: threading.Thread = None + self._ready_signal: threading.Event = threading.Event() + self._done_signal: asyncio.Event = None + self._conn_exception = False + self._behavior_control_level = behavior_control_level + self.active_commands = [] + + @property + def loop(self) -> asyncio.BaseEventLoop: + """A direct reference to the loop on the connection thread. + Can be used to run functions in on thread. + + .. testcode:: + + import anki_vector + import asyncio + + async def connection_function(): + print("I'm running in the connection thread event loop.") + + with anki_vector.Robot() as robot: + asyncio.run_coroutine_threadsafe(connection_function(), robot.conn.loop) + + :returns: The loop running inside the connection thread + """ + if self._loop is None: + raise VectorAsyncException("Attempted to access the connection loop before it was ready") + return self._loop + + @property + def thread(self) -> threading.Thread: + """A direct reference to the connection thread. Available to callers to determine if the + current thread is the connection thread. + + .. testcode:: + + import anki_vector + import threading + + with anki_vector.Robot() as robot: + if threading.current_thread() is robot.conn.thread: + print("This code is running on the connection thread") + else: + print("This code is not running on the connection thread") + + :returns: The connection thread where all of the grpc messages are being processed. + """ + if self._thread is None: + raise VectorAsyncException("Attempted to access the connection loop before it was ready") + return self._thread + + @property + def grpc_interface(self) -> client.ExternalInterfaceStub: + """A direct reference to the connected aiogrpc interface. + + This may be used to directly call grpc messages bypassing :class:`anki_vector.Robot` + + .. code-block:: python + + import anki_vector + + # Connect to your Vector + conn = anki_vector.connection.Connection("Vector-XXXX", "XX.XX.XX.XX:443", "/path/to/file.cert", "") + conn.connect() + # Run your commands + async def play_animation(): + # Run your commands + anim = anki_vector.messaging.protocol.Animation(name="anim_pounce_success_02") + anim_request = anki_vector.messaging.protocol.PlayAnimationRequest(animation=anim) + return await conn.grpc_interface.PlayAnimation(anim_request) # This needs to be run in an asyncio loop + conn.run_coroutine(play_animation()).result() + # Close the connection + conn.close() + """ + return self._interface + + @property + def behavior_control_level(self) -> ControlPriorityLevel: + """Returns the specific :class:`ControlPriorityLevel` requested for behavior control. + + To be able to directly control Vector's motors, override his screen, play an animation, etc., + the :class:`Connection` will need behavior control. This property identifies the enumerated + level of behavior control that the SDK will maintain over the robot. + + For more information about behavior control, see :ref:`behavior `. + + .. code-block:: python + + import anki_vector + + with anki_vector.Robot() as robot: + print(robot.conn.behavior_control_level) # Will print ControlPriorityLevel.DEFAULT_PRIORITY + robot.conn.release_control() + print(robot.conn.behavior_control_level) # Will print None + """ + return self._behavior_control_level + + @property + def requires_behavior_control(self) -> bool: + """True if the :class:`Connection` requires behavior control. + + To be able to directly control Vector's motors, override his screen, play an animation, etc., + the :class:`Connection` will need behavior control. This boolean signifies that + the :class:`Connection` will try to maintain control of Vector's behavior system even after losing + control to higher priority robot behaviors such as returning home to charge a low battery. + + For more information about behavior control, see :ref:`behavior `. + + .. code-block:: python + + import time + + import anki_vector + + def callback(robot, event_type, event): + robot.conn.request_control() + print(robot.conn.requires_behavior_control) # Will print True + robot.anim.play_animation_trigger('GreetAfterLongTime') + robot.conn.release_control() + + with anki_vector.Robot(behavior_control_level=None) as robot: + print(robot.conn.requires_behavior_control) # Will print False + robot.events.subscribe(callback, anki_vector.events.Events.robot_observed_face) + + # Waits 10 seconds. Show Vector your face. + time.sleep(10) + """ + return self._behavior_control_level is not None + + @property + def control_lost_event(self) -> asyncio.Event: + """This provides an :class:`asyncio.Event` that a user may :func:`wait()` upon to + detect when Vector has taken control of the behavior system at a higher priority. + + .. testcode:: + + import anki_vector + + async def auto_reconnect(conn: anki_vector.connection.Connection): + await conn.control_lost_event.wait() + conn.request_control() + """ + return self._control_events.lost_event + + @property + def control_granted_event(self) -> asyncio.Event: + """This provides an :class:`asyncio.Event` that a user may :func:`wait()` upon to + detect when Vector has given control of the behavior system to the SDK program. + + .. testcode:: + + import anki_vector + + async def wait_for_control(conn: anki_vector.connection.Connection): + await conn.control_granted_event.wait() + # Run commands that require behavior control + """ + return self._control_events.granted_event + + def request_control(self, behavior_control_level: ControlPriorityLevel = ControlPriorityLevel.DEFAULT_PRIORITY, timeout: float = 10.0): + """Explicitly request behavior control. Typically used after detecting :func:`control_lost_event`. + + To be able to directly control Vector's motors, override his screen, play an animation, etc., + the :class:`Connection` will need behavior control. This function will acquire control + of Vector's behavior system. This will raise a :class:`VectorControlTimeoutException` if it fails + to gain control before the timeout. + + For more information about behavior control, see :ref:`behavior ` + + .. testcode:: + + import anki_vector + + async def auto_reconnect(conn: anki_vector.connection.Connection): + await conn.control_lost_event.wait() + conn.request_control(timeout=5.0) + + :param timeout: The time allotted to attempt a connection, in seconds. + :param behavior_control_level: request control of Vector's behavior system at a specific level of control. + See :class:`ControlPriorityLevel` for more information. + """ + if not isinstance(behavior_control_level, ControlPriorityLevel): + raise TypeError("behavior_control_level must be of type ControlPriorityLevel") + if self._thread is threading.current_thread(): + return asyncio.ensure_future(self._request_control(behavior_control_level=behavior_control_level, timeout=timeout), loop=self._loop) + return self.run_coroutine(self._request_control(behavior_control_level=behavior_control_level, timeout=timeout)) + + async def _request_control(self, behavior_control_level: ControlPriorityLevel = ControlPriorityLevel.DEFAULT_PRIORITY, timeout: float = 10.0): + self._behavior_control_level = behavior_control_level + self._control_events.request(self._behavior_control_level) + try: + self._has_control = await asyncio.wait_for(self.control_granted_event.wait(), timeout) + except futures.TimeoutError as e: + raise VectorControlTimeoutException(f"Surpassed timeout of {timeout}s") from e + + def release_control(self, timeout: float = 10.0): + """Explicitly release control. Typically used after detecting :func:`control_lost_event`. + + To be able to directly control Vector's motors, override his screen, play an animation, etc., + the :class:`Connection` will need behavior control. This function will release control + of Vector's behavior system. This will raise a :class:`VectorControlTimeoutException` if it fails + to receive a control_lost event before the timeout. + + .. testcode:: + + import anki_vector + + async def wait_for_control(conn: anki_vector.connection.Connection): + await conn.control_granted_event.wait() + # Run commands that require behavior control + conn.release_control() + + :param timeout: The time allotted to attempt to release control, in seconds. + """ + if self._thread is threading.current_thread(): + return asyncio.ensure_future(self._release_control(timeout=timeout), loop=self._loop) + return self.run_coroutine(self._release_control(timeout=timeout)) + + async def _release_control(self, timeout: float = 10.0): + self._behavior_control_level = None + self._control_events.release() + try: + self._has_control = await asyncio.wait_for(self.control_lost_event.wait(), timeout) + except futures.TimeoutError as e: + raise VectorControlTimeoutException(f"Surpassed timeout of {timeout}s") from e + + def connect(self, timeout: float = 10.0) -> None: + """Connect to Vector. This will start the connection thread which handles all messages + between Vector and Python. + + .. code-block:: python + + import anki_vector + + # Connect to your Vector + conn = anki_vector.connection.Connection("Vector-XXXX", "XX.XX.XX.XX:443", "/path/to/file.cert", "") + conn.connect() + # Run your commands + async def play_animation(): + # Run your commands + anim = anki_vector.messaging.protocol.Animation(name="anim_pounce_success_02") + anim_request = anki_vector.messaging.protocol.PlayAnimationRequest(animation=anim) + return await conn.grpc_interface.PlayAnimation(anim_request) # This needs to be run in an asyncio loop + conn.run_coroutine(play_animation()).result() + # Close the connection + conn.close() + + :param timeout: The time allotted to attempt a connection, in seconds. + """ + if self._thread: + raise VectorAsyncException("\n\nRepeated connections made to open Connection.") + self._ready_signal.clear() + self._thread = threading.Thread(target=self._connect, args=(timeout,), daemon=True, name="gRPC Connection Handler Thread") + self._thread.start() + ready = self._ready_signal.wait(timeout=4 * timeout) + if not ready: + raise VectorNotFoundException() + if hasattr(self._ready_signal, "exception"): + e = getattr(self._ready_signal, "exception") + delattr(self._ready_signal, "exception") + raise e + + def _connect(self, timeout: float) -> None: + """The function that runs on the connection thread. This will connect to Vector, + and establish the BehaviorControl stream. + """ + try: + if threading.main_thread() is threading.current_thread(): + raise VectorAsyncException("\n\nConnection._connect must be run outside of the main thread.") + self._loop = asyncio.new_event_loop() + asyncio.set_event_loop(self._loop) + self._done_signal = asyncio.Event() + if not self._behavior_control_level: + self._control_events = _ControlEventManager(self._loop) + else: + self._control_events = _ControlEventManager(self._loop, priority=self._behavior_control_level) + + trusted_certs = None + if not self.cert_file is None: + with open(self.cert_file, 'rb') as cert: + trusted_certs = cert.read() + else: + if not self._escape_pod: + raise VectorConfigurationException("Must provide a cert file to authenticate to Vector.") + + if self._escape_pod: + if not EscapePod.validate_certificate_name(self.cert_file, self.name): + trusted_certs = EscapePod.get_authentication_certificate(self.host) + self.name = EscapePod.get_certificate_name(trusted_certs) + self._guid = EscapePod.authenticate_escape_pod(self.host, self.name, trusted_certs) + + # Pin the robot certificate for opening the channel + channel_credentials = aiogrpc.ssl_channel_credentials(root_certificates=trusted_certs) + # Add authorization header for all the calls + call_credentials = aiogrpc.access_token_call_credentials(self._guid) + + credentials = aiogrpc.composite_channel_credentials(channel_credentials, call_credentials) + + self._logger.info(f"Connecting to {self.host} for {self.name} using {self.cert_file}") + self._channel = aiogrpc.secure_channel(self.host, credentials, + options=(("grpc.ssl_target_name_override", self.name,),)) + + # Verify the connection to Vector is able to be established (client-side) + try: + # Explicitly grab _channel._channel to test the underlying grpc channel directly + grpc.channel_ready_future(self._channel._channel).result(timeout=timeout) # pylint: disable=protected-access + except grpc.FutureTimeoutError as e: + raise VectorNotFoundException() from e + + self._interface = client.ExternalInterfaceStub(self._channel) + + # Verify Vector and the SDK have compatible protocol versions + version = protocol.ProtocolVersionRequest(client_version=protocol.PROTOCOL_VERSION_CURRENT, min_host_version=protocol.PROTOCOL_VERSION_MINIMUM) + protocol_version = self._loop.run_until_complete(self._interface.ProtocolVersion(version)) + if protocol_version.result != protocol.ProtocolVersionResponse.SUCCESS or protocol.PROTOCOL_VERSION_MINIMUM > protocol_version.host_version: # pylint: disable=no-member + raise VectorInvalidVersionException(protocol_version) + + self._control_stream_task = self._loop.create_task(self._open_connections()) + + # Initialze SDK + sdk_module_version = __version__ + python_version = platform.python_version() + python_implementation = platform.python_implementation() + os_version = platform.platform() + cpu_version = platform.machine() + initialize = protocol.SDKInitializationRequest(sdk_module_version=sdk_module_version, + python_version=python_version, + python_implementation=python_implementation, + os_version=os_version, + cpu_version=cpu_version) + self._loop.run_until_complete(self._interface.SDKInitialization(initialize)) + + if self._behavior_control_level: + self._loop.run_until_complete(self._request_control(behavior_control_level=self._behavior_control_level, timeout=timeout)) + except grpc.RpcError as rpc_error: # pylint: disable=broad-except + setattr(self._ready_signal, "exception", connection_error(rpc_error)) + self._loop.close() + return + except Exception as e: # pylint: disable=broad-except + # Propagate the errors to the calling thread + setattr(self._ready_signal, "exception", e) + self._loop.close() + return + finally: + self._ready_signal.set() + + try: + async def wait_until_done(): + return await self._done_signal.wait() + self._loop.run_until_complete(wait_until_done()) + finally: + self._loop.close() + + async def _request_handler(self): + """Handles generating messages for the BehaviorControl stream.""" + while await self._control_events.request_event.wait(): + self._control_events.request_event.clear() + if self._control_events.is_shutdown: + return + priority = self._control_events.priority + if priority is None: + msg = protocol.ControlRelease() + msg = protocol.BehaviorControlRequest(control_release=msg) + else: + msg = protocol.ControlRequest(priority=priority.value) + msg = protocol.BehaviorControlRequest(control_request=msg) + self._logger.debug(f"BehaviorControl {MessageToString(msg, as_one_line=True)}") + yield msg + + async def _open_connections(self): + """Starts the BehaviorControl stream, and handles the messages coming back from the robot.""" + try: + async for response in self._interface.BehaviorControl(self._request_handler()): + response_type = response.WhichOneof("response_type") + if response_type == 'control_granted_response': + self._logger.info(f"BehaviorControl {MessageToString(response, as_one_line=True)}") + self._control_events.update(True) + elif response_type == 'control_lost_event': + self._cancel_active() + self._logger.info(f"BehaviorControl {MessageToString(response, as_one_line=True)}") + self._control_events.update(False) + except futures.CancelledError: + self._logger.debug('Behavior handler task was cancelled. This is expected during disconnection.') + + def _cancel_active(self): + for fut in self.active_commands: + if not fut.done(): + fut.cancel() + self.active_commands = [] + + def close(self): + """Cleanup the connection, and shutdown all the event handlers. + + Usually this should be invoked by the Robot class when it closes. + + .. code-block:: python + + import anki_vector + + # Connect to your Vector + conn = anki_vector.connection.Connection("Vector-XXXX", "XX.XX.XX.XX:443", "/path/to/file.cert", "") + conn.connect() + # Run your commands + async def play_animation(): + # Run your commands + anim = anki_vector.messaging.protocol.Animation(name="anim_pounce_success_02") + anim_request = anki_vector.messaging.protocol.PlayAnimationRequest(animation=anim) + return await conn.grpc_interface.PlayAnimation(anim_request) # This needs to be run in an asyncio loop + conn.run_coroutine(play_animation()).result() + # Close the connection + conn.close() + """ + try: + if self._control_events: + self._control_events.shutdown() + if self._control_stream_task: + self._control_stream_task.cancel() + self.run_coroutine(self._control_stream_task).result() + self._cancel_active() + if self._channel: + self.run_coroutine(self._channel.close()).result() + self.run_coroutine(self._done_signal.set) + self._thread.join(timeout=5) + except: + pass + finally: + self._thread = None + + def run_soon(self, coro: Awaitable) -> None: + """Schedules the given awaitable to run on the event loop for the connection thread. + + .. testcode:: + + import anki_vector + import time + + async def my_coroutine(): + print("Running on the connection thread") + + with anki_vector.Robot() as robot: + robot.conn.run_soon(my_coroutine()) + time.sleep(1) + + :param coro: The coroutine, task or any awaitable to schedule for execution on the connection thread. + """ + if coro is None or not inspect.isawaitable(coro): + raise VectorAsyncException(f"\n\n{coro.__name__ if hasattr(coro, '__name__') else coro} is not awaitable, so cannot be ran with run_soon.\n") + + def soon(): + try: + asyncio.ensure_future(coro) + except TypeError as e: + raise VectorAsyncException(f"\n\n{coro.__name__ if hasattr(coro, '__name__') else coro} could not be ensured as a future.\n") from e + if threading.current_thread() is self._thread: + self._loop.call_soon(soon) + else: + self._loop.call_soon_threadsafe(soon) + + def run_coroutine(self, coro: Awaitable) -> Any: + """Runs a given awaitable on the connection thread's event loop. + Cannot be called from within the connection thread. + + .. testcode:: + + import anki_vector + + async def my_coroutine(): + print("Running on the connection thread") + return "Finished" + + with anki_vector.Robot() as robot: + result = robot.conn.run_coroutine(my_coroutine()) + + :param coro: The coroutine, task or any other awaitable which should be executed. + :returns: The result of the awaitable's execution. + """ + if threading.current_thread() is self._thread: + raise VectorAsyncException("Attempting to invoke async from same thread." + "Instead you may want to use 'run_soon'") + if asyncio.iscoroutinefunction(coro) or asyncio.iscoroutine(coro): + return self._run_coroutine(coro) + if asyncio.isfuture(coro): + async def future_coro(): + return await coro + return self._run_coroutine(future_coro()) + if callable(coro): + async def wrapped_coro(): + return coro() + return self._run_coroutine(wrapped_coro()) + raise VectorAsyncException("\n\nInvalid parameter to run_coroutine: {}\n" + "This function expects a coroutine, task, or awaitable.".format(type(coro))) + + def _run_coroutine(self, coro): + return asyncio.run_coroutine_threadsafe(coro, self._loop) + + +def on_connection_thread(log_messaging: bool = True, requires_control: bool = True, is_cancellable: CancelType = None) -> Callable[[Coroutine[util.Component, Any, None]], Any]: + """A decorator generator used internally to denote which functions will run on + the connection thread. This unblocks the caller of the wrapped function + and allows them to continue running while the messages are being processed. + + .. code-block:: python + + import anki_vector + + class MyComponent(anki_vector.util.Component): + @connection._on_connection_thread() + async def on_connection_thread(self): + # Do work on the connection thread + + :param log_messaging: True if the log output should include the entire message or just the size. Recommended for + large binary return values. + :param requires_control: True if the function should wait until behavior control is granted before executing. + :param is_cancellable: use a valid enum of :class:`CancelType` to specify the type of cancellation for the + function. Defaults to 'None' implying no support for responding to cancellation. + :returns: A decorator which has 3 possible returns based on context: the result of the decorated function, + the :class:`concurrent.futures.Future` which points to the decorated function, or the + :class:`asyncio.Future` which points to the decorated function. + These contexts are: when the robot is a :class:`~anki_vector.robot.Robot`, + when the robot is an :class:`~anki_vector.robot.AsyncRobot`, and when + called from the connection thread respectively. + """ + def _on_connection_thread_decorator(func: Coroutine) -> Any: + """A decorator which specifies a function to be executed on the connection thread + + :params func: The function to be decorated + :returns: There are 3 possible returns based on context: the result of the decorated function, + the :class:`concurrent.futures.Future` which points to the decorated function, or the + :class:`asyncio.Future` which points to the decorated function. + These contexts are: when the robot is a :class:`anki_vector.robot.Robot`, + when the robot is an :class:`anki_vector.robot.AsyncRobot`, and when + called from the connection thread respectively. + """ + if not asyncio.iscoroutinefunction(func): + raise VectorAsyncException("\n\nCannot define non-coroutine function '{}' to run on connection thread.\n" + "Make sure the function is defined using 'async def'.".format(func.__name__ if hasattr(func, "__name__") else func)) + + @functools.wraps(func) + async def log_handler(conn: Connection, func: Coroutine, logger: logging.Logger, *args: List[Any], **kwargs: Dict[str, Any]) -> Coroutine: + """Wrap the provided coroutine to better express exceptions as specific :class:`anki_vector.exceptions.VectorException`s, and + adds logging to incoming (from the robot) and outgoing (to the robot) messages. + """ + result = None + # TODO: only have the request wait for control if we're not done. If done raise an exception. + control = conn.control_granted_event + if requires_control and not control.is_set(): + if not conn.requires_behavior_control: + raise VectorControlException(func.__name__) + logger.info(f"Delaying {func.__name__} until behavior control is granted") + await asyncio.wait([conn.control_granted_event.wait()], timeout=10) + message = args[1:] + outgoing = message if log_messaging else "size = {} bytes".format(sys.getsizeof(message)) + logger.debug(f'Outgoing {func.__name__}: {outgoing}') + try: + result = await func(*args, **kwargs) + except grpc.RpcError as rpc_error: + raise connection_error(rpc_error) from rpc_error + incoming = str(result).strip() if log_messaging else "size = {} bytes".format(sys.getsizeof(result)) + logger.debug(f'Incoming {func.__name__}: {type(result).__name__} {incoming}') + return result + + @functools.wraps(func) + def result(*args: List[Any], **kwargs: Dict[str, Any]) -> Any: + """The function that is the result of the decorator. Provides a wrapped function. + + :param _return_future: A hidden parameter which allows the wrapped function to explicitly + return a future (default for AsyncRobot) or not (default for Robot). + :returns: Based on context this can return the result of the decorated function, + the :class:`concurrent.futures.Future` which points to the decorated function, or the + :class:`asyncio.Future` which points to the decorated function. + These contexts are: when the robot is a :class:`anki_vector.robot.Robot`, + when the robot is an :class:`anki_vector.robot.AsyncRobot`, and when + called from the connection thread respectively.""" + self = args[0] # Get the self reference from the function call + # if the call supplies a _return_future parameter then override force_async with that. + _return_future = kwargs.pop('_return_future', self.force_async) + + action_id = None + if is_cancellable == CancelType.CANCELLABLE_ACTION: + action_id = self._get_next_action_id() + kwargs['_action_id'] = action_id + + wrapped_coroutine = log_handler(self.conn, func, self.logger, *args, **kwargs) + + if threading.current_thread() == self.conn.thread: + if self.conn.loop.is_running(): + return asyncio.ensure_future(wrapped_coroutine, loop=self.conn.loop) + raise VectorAsyncException("\n\nThe connection thread loop is not running, but a " + "function '{}' is being invoked on that thread.\n".format(func.__name__ if hasattr(func, "__name__") else func)) + future = asyncio.run_coroutine_threadsafe(wrapped_coroutine, self.conn.loop) + + if is_cancellable == CancelType.CANCELLABLE_ACTION: + def user_cancelled_action(fut): + if action_id is None: + return + + if fut.cancelled(): + self._abort_action(action_id) + + future.add_done_callback(user_cancelled_action) + + if is_cancellable == CancelType.CANCELLABLE_BEHAVIOR: + def user_cancelled_behavior(fut): + if fut.cancelled(): + self._abort_behavior() + + future.add_done_callback(user_cancelled_behavior) + + if requires_control: + self.conn.active_commands.append(future) + + def clear_when_done(fut): + if fut in self.conn.active_commands: + self.conn.active_commands.remove(fut) + future.add_done_callback(clear_when_done) + if _return_future: + return future + try: + return future.result() + except futures.CancelledError: + self.logger.warning(f"{func.__name__} cancelled because behavior control was lost") + return None + return result + return _on_connection_thread_decorator diff --git a/anki_vector/opengl/__init__.py b/anki_vector/opengl/__init__.py index bc9c542..f4eb8f5 100644 --- a/anki_vector/opengl/__init__.py +++ b/anki_vector/opengl/__init__.py @@ -24,9 +24,9 @@ This package requires Python to have the PyOpenGL package installed, along with an implementation of GLUT (OpenGL Utility Toolkit). - To install the Python packages on Mac and Linux do ``python3 -m pip install --user "anki_vector[3dviewer]"`` + To install the Python packages on Mac and Linux do ``python3 -m pip install --user "cyb3r_vector_sdk[3dviewer]"`` - To install the Python packages on Windows do ``py -3 -m pip install --user "anki_vector[3dviewer]"`` + To install the Python packages on Windows do ``py -3 -m pip install --user "cyb3r_vector_sdk[3dviewer]"`` On Windows and Linux you must also install freeglut (macOS / OSX has one preinstalled). diff --git a/anki_vector/opengl/opengl.py b/anki_vector/opengl/opengl.py index b387e07..2643b51 100644 --- a/anki_vector/opengl/opengl.py +++ b/anki_vector/opengl/opengl.py @@ -21,9 +21,9 @@ This package requires Python to have the PyOpenGL package installed, along with an implementation of GLUT (OpenGL Utility Toolkit). - To install the Python packages on Mac and Linux do ``python3 -m pip install --user "anki_vector[3dviewer]"`` + To install the Python packages on Mac and Linux do ``python3 -m pip install --user "cyb3r_vector_sdk[3dviewer]"`` - To install the Python packages on Windows do ``py -3 -m pip install --user "anki_vector[3dviewer]"`` + To install the Python packages on Windows do ``py -3 -m pip install --user "cyb3r_vector_sdk[3dviewer]"`` On Windows and Linux you must also install freeglut (macOS / OSX has one preinstalled). @@ -62,7 +62,7 @@ def raise_opengl_or_pillow_import_error(opengl_import_exc): % opengl_import_exc) raise NotImplementedError('OpenGL is not available; ' 'make sure the PyOpenGL and Pillow packages are installed:\n' - 'Do `pip3 install --user "anki_vector[3dviewer]"` to install. Error: %s' % opengl_import_exc) + 'Do `pip3 install --user "cyb3r_vector_sdk[3dviewer]"` to install. Error: %s' % opengl_import_exc) try: diff --git a/anki_vector/opengl/opengl_vector.py b/anki_vector/opengl/opengl_vector.py index 702cb27..180e1bc 100644 --- a/anki_vector/opengl/opengl_vector.py +++ b/anki_vector/opengl/opengl_vector.py @@ -1,4 +1,4 @@ -# Copyright (c) 2018 Anki, Inc. +py# Copyright (c) 2018 Anki, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -18,9 +18,9 @@ This package requires Python to have the PyOpenGL package installed, along with an implementation of GLUT (OpenGL Utility Toolkit). - To install the Python packages on Mac and Linux do ``python3 -m pip install --user "anki_vector[3dviewer]"`` + To install the Python packages on Mac and Linux do ``python3 -m pip install --user "cyb3r_vector_sdk[3dviewer]"`` - To install the Python packages on Windows do ``py -3 -m pip install --user "anki_vector[3dviewer]"`` + To install the Python packages on Windows do ``py -3 -m pip install --user "cyb3r_vector_sdk[3dviewer]"`` On Windows and Linux you must also install freeglut (macOS / OSX has one preinstalled). diff --git a/anki_vector/opengl/opengl_viewer.py b/anki_vector/opengl/opengl_viewer.py index e18f9b1..8274c6a 100644 --- a/anki_vector/opengl/opengl_viewer.py +++ b/anki_vector/opengl/opengl_viewer.py @@ -35,9 +35,9 @@ This package requires Python to have the PyOpenGL package installed, along with an implementation of GLUT (OpenGL Utility Toolkit). - To install the Python packages on Mac and Linux do ``python3 -m pip install --user "anki_vector[3dviewer]"`` + To install the Python packages on Mac and Linux do ``python3 -m pip install --user "cyb3r_vector_sdk[3dviewer]"`` - To install the Python packages on Windows do ``py -3 -m pip install --user "anki_vector[3dviewer]"`` + To install the Python packages on Windows do ``py -3 -m pip install --user "cyb3r_vector_sdk[3dviewer]"`` On Windows and Linux you must also install freeglut (macOS / OSX has one preinstalled). diff --git a/anki_vector/version.py b/anki_vector/version.py index 62b4cfd..c8fa3ac 100644 --- a/anki_vector/version.py +++ b/anki_vector/version.py @@ -1,15 +1,15 @@ -# Copyright (c) 2018 Anki, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License in the file LICENSE.txt or at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -__version__ = "0.7.2.dev3" +# Copyright (c) 2018 Anki, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License in the file LICENSE.txt or at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__version__ = "0.7.2.dev4" diff --git a/examples/tutorials/00_hello_escapepod.py b/examples/tutorials/00_hello_escapepod.py index 5465420..b3e05db 100644 --- a/examples/tutorials/00_hello_escapepod.py +++ b/examples/tutorials/00_hello_escapepod.py @@ -1,32 +1,32 @@ -#!/usr/bin/env python3 - -# Copyright (c) 2021 cyb3rdog -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License in the file LICENSE.txt or at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Hello Escape Pod - -Make Vector say 'Hello Escape Pod' in this simple Vector SDK example program. -""" - -import anki_vector - - -def main(): - with anki_vector.Robot(ip="192.168.0.148", escape_pod=True) as robot: - print("Say 'Hello Escape Pod'...") - robot.behavior.say_text("Hello Escape Pod") - - -if __name__ == "__main__": - main() +#!/usr/bin/env python3 + +# Copyright (c) 2021 cyb3rdog +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License in the file LICENSE.txt or at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Hello Escape Pod + +Make Vector say 'Hello Escape Pod' in this simple Vector SDK example program. +""" + +import anki_vector + + +def main(): + with anki_vector.Robot(ip="192.168.0.148", escape_pod=True) as robot: + print("Say 'Hello Escape Pod'...") + robot.behavior.say_text("Hello Escape Pod") + + +if __name__ == "__main__": + main() diff --git a/examples/tutorials/10_play_audio.py b/examples/tutorials/10_play_audio.py index 499989f..3818a72 100755 --- a/examples/tutorials/10_play_audio.py +++ b/examples/tutorials/10_play_audio.py @@ -28,8 +28,8 @@ def main(): # https://github.com/anki/vector-python-sdk/blob/master/examples/sounds/vector_bell_whistle.wav # # Paste these two wav files next to this tutorial to play sounds. - robot.audio.stream_wav_file("vector_bell_whistle.wav", 75) - robot.audio.stream_wav_file("vector_alert.wav", 75) + robot.audio.stream_wav_file("../sounds/vector_bell_whistle.wav", 75) + robot.audio.stream_wav_file("../sounds/vector_alert.wav", 75) if __name__ == "__main__": From 5d6c3ab6893829956341d72646cc183ba3e91daf Mon Sep 17 00:00:00 2001 From: cyb3rdog Date: Tue, 27 Apr 2021 23:16:07 +0200 Subject: [PATCH 09/15] Update README.md --- README.md | 37 +++++++++++++++++++++---------------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index b02799e..3a4f778 100644 --- a/README.md +++ b/README.md @@ -12,8 +12,9 @@ This is the extended fork of the original Anki Vector Python SDK. ## Getting Started -For the steps undocumented here, you can still refer to [this original SDK documentation](https://developer.anki.com/vector/docs/index.html). +For the steps undocumented here, you can still refer to [this original SDK documentation](https://developer.anki.com/vector/docs/index.html). *(TODO: docs are old and needs to be updated, contributors wanted)* + If you are new to the Vector's Python SDK, refer to this documentation anyways, as it still contains lots of valuable information. @@ -21,41 +22,45 @@ If you are new to the Vector's Python SDK, refer to this documentation anyways, #### Windows: -If you dont have python installed yet, download and install it from the [Python.org](https://www.python.org/downloads/windows/) -Be sure to tick the “Add Python 3.X to PATH” checkbox on the Setup screen. +If you dont have python installed yet, download and install it from the [Python.org](https://www.python.org/downloads/windows/). +Make sure to tick the “Add Python 3.X to PATH†checkbox on the Setup screen. To avoid dificulties during the SDK install on your existing python installation, open the command line and run: -```py -m pip install -U pip``` -```py -m pip install --upgrade setuptools``` + +``` +py -m pip install -U pip +py -m pip install --upgrade setuptools +``` #### Linux: Open the Terminal and run following commands to install and update the Python, and packages required by SDK: -```sudo apt-get update``` -```sudo apt-get install -y python3 python3-pip python3-tk python3-pil.imagetk build-essential libssl-dev libffi-dev freeglut3``` - -```pip3 install --upgrade setuptools``` +``` +sudo apt-get update +sudo apt-get install -y python3 python3-pip python3-tk python3-pil.imagetk build-essential libssl-dev libffi-dev freeglut3 +pip3 install --upgrade setuptools +``` ### SDK Installation -- Note: Use either **```pip```** or **```pip3```** correspondingly to the Python version you are using. + - Note: Use either **```pip```** or **```pip3```** correspondingly to the Python version you are using. In case you have previously installed the original ***Anki*** or ***Ikkez*** SDK, uninstall it/them with following command(s): -```pip uninstall anki_vector``` or ```pip3 uninstall anki_vector``` -```pip uninstall ikkez_vector``` or ```pip3 uninstall ikkez_vector``` +- ```pip uninstall anki_vector``` or ```pip3 uninstall anki_vector``` +- ```pip uninstall ikkez_vector``` or ```pip3 uninstall ikkez_vector``` To install this new SDK, run: -```pip install cyb3r_vector_sdk``` or ```pip3 install cyb3r_vector_sdk``` -and -```pip install --user "cyb3r_vector_sdk[3dviewer]"``` or ```pip3 install --user "cyb3r_vector_sdk[3dviewer]"``` +- ```pip install cyb3r_vector_sdk``` or ```pip3 install cyb3r_vector_sdk``` +and +- ```pip install --user "cyb3r_vector_sdk[3dviewer]"``` or ```pip3 install --user "cyb3r_vector_sdk[3dviewer]"``` To upgrade this SDK to its latest version, use: -```pip install cyb3r_vector_sdk --upgrade``` or ```pip3 install cyb3r_vector_sdk --upgrade``` +- ```pip install cyb3r_vector_sdk --upgrade``` or ```pip3 install cyb3r_vector_sdk --upgrade``` If you want to know where the SDK files are installed, use following command: From aa8658d6c3cd139b77b7aef1fadff37ec4402bcb Mon Sep 17 00:00:00 2001 From: cyb3rdog Date: Tue, 27 Apr 2021 23:16:50 +0200 Subject: [PATCH 10/15] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3a4f778..dd6e6e3 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ To install this new SDK, run: - ```pip install cyb3r_vector_sdk``` or ```pip3 install cyb3r_vector_sdk``` and -- ```pip install --user "cyb3r_vector_sdk[3dviewer]"``` or ```pip3 install --user "cyb3r_vector_sdk[3dviewer]"``` +- ```pip install "cyb3r_vector_sdk[3dviewer]"``` or ```pip3 install "cyb3r_vector_sdk[3dviewer]"``` To upgrade this SDK to its latest version, use: From 0bdd5cdec16c986ab7b65472a3f3d25a3ae88a56 Mon Sep 17 00:00:00 2001 From: cyb3rdog Date: Wed, 28 Apr 2021 00:20:59 +0200 Subject: [PATCH 11/15] 0.7.2.dev5 - setup.py --- setup.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 745aad0..5966abb 100644 --- a/setup.py +++ b/setup.py @@ -13,6 +13,14 @@ # limitations under the License. """ +Anki/DDL Vector - Python SDK: + by cyb3rdog + +## With support for Production, EscapePod and OSKR robots! ## +This is the extended fork of the original Anki Vector Python SDK. + +For more information, please visit the project Github site: https://github.com/cyb3rdog/vector-python-sdk + The Vector SDK gives you direct access to Vector's unprecedented set of advanced sensors, AI capabilities, and robotics technologies including computer vision, intelligent mapping and navigation, and a groundbreaking collection of expressive animations. It's powerful but easy to use, complex but not complicated, and versatile enough to be used across a wide range of domains including enterprise, research, and entertainment. Find out more at https://developer.anki.com @@ -22,7 +30,9 @@ Official developer forum: https://forums.anki.com/ Requirements: - * Python 3.6.1 or later + * Python 3.6.1 + * Python 3.7 + * Python 3.8 """ import os.path From 9929bb897ea0dabc12642c915679570f58cf4065 Mon Sep 17 00:00:00 2001 From: Frederico Wu Date: Wed, 5 May 2021 22:56:47 -0400 Subject: [PATCH 12/15] ip and escape_pod parameters read from sdk config --- anki_vector/robot.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/anki_vector/robot.py b/anki_vector/robot.py index f612ec5..2d14d81 100755 --- a/anki_vector/robot.py +++ b/anki_vector/robot.py @@ -111,7 +111,7 @@ def __init__(self, ip: str = None, name: str = None, config: dict = None, - escape_pod: bool = False, + escape_pod: bool = None, default_logging: bool = True, behavior_activation_timeout: int = 10, cache_animation_lists: bool = True, @@ -128,8 +128,8 @@ def __init__(self, self.logger = util.get_class_logger(__name__, self) self._force_async = False config = config if config is not None else {} - if not escape_pod or ip is {}: - config = {**util.read_configuration(serial, name, self.logger, escape_pod), **config} + config = {**util.read_configuration(serial, name, self.logger, escape_pod or False), **config} + escape_pod = config.get("escape_pod") if escape_pod is None else escape_pod if name is not None: vector_mdns = VectorMdns.find_vector(name) @@ -142,7 +142,7 @@ def __init__(self, self._cert_file = config["cert"] if 'cert' in config else None self._guid = config["guid"] if 'guid' in config else None self._port = config["port"] if 'port' in config else "443" - self._ip = ip if ip is not {} else None + self._ip = ip or config.get("ip") if self._ip is None and 'ip' in config: self._ip = config["ip"] From 09f3bbf58b41875838b89a5cfce2730ab5461ede Mon Sep 17 00:00:00 2001 From: Frederico Wu Date: Wed, 5 May 2021 23:04:14 -0400 Subject: [PATCH 13/15] ip and escape_pod parameters read from sdk config --- anki_vector/robot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/anki_vector/robot.py b/anki_vector/robot.py index 2d14d81..5a9724c 100755 --- a/anki_vector/robot.py +++ b/anki_vector/robot.py @@ -129,7 +129,7 @@ def __init__(self, self._force_async = False config = config if config is not None else {} config = {**util.read_configuration(serial, name, self.logger, escape_pod or False), **config} - escape_pod = config.get("escape_pod") if escape_pod is None else escape_pod + escape_pod = config.get("escape_pod", False) if escape_pod is None else escape_pod if name is not None: vector_mdns = VectorMdns.find_vector(name) From 712b834a61f93864ae4a3a1cfd470ab290909dca Mon Sep 17 00:00:00 2001 From: cyb3rdog Date: Tue, 15 Jun 2021 22:30:34 +0200 Subject: [PATCH 14/15] merge Read the ip and escape_pod parameters from the sdk init file --- README.md | 29 +++++++++++++++++++++++++---- anki_vector/version.py | 2 +- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index dd6e6e3..f3bb2be 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ - + # Anki/DDL Vector - Python SDK by cyb3rdog @@ -12,7 +12,7 @@ This is the extended fork of the original Anki Vector Python SDK. ## Getting Started -For the steps undocumented here, you can still refer to [this original SDK documentation](https://developer.anki.com/vector/docs/index.html). +For the steps undocumented here, you can still refer to [this original SDK documentation](https://developer.anki.com/vector/docs/index.html). *(TODO: docs are old and needs to be updated, contributors wanted)* If you are new to the Vector's Python SDK, refer to this documentation anyways, as it still contains lots of valuable information. @@ -22,7 +22,7 @@ If you are new to the Vector's Python SDK, refer to this documentation anyways, #### Windows: -If you dont have python installed yet, download and install it from the [Python.org](https://www.python.org/downloads/windows/). +If you dont have python installed yet, download and install it from the [Python.org](https://www.python.org/downloads/windows/). Make sure to tick the “Add Python 3.X to PATH†checkbox on the Setup screen. To avoid dificulties during the SDK install on your existing python installation, open the command line and run: @@ -84,7 +84,7 @@ To configure the Python SDK for **EscapePod**, and/or **EP+OSKR** robots, run: ### SDK Usage - EscapePod -You can either use the ```anki_vector.configure_pod``` in order to save your authentication into the sdk_config.ini file, and use all the [examples](https://github.com/cyb3rdog/vector-python-sdk/tree/master/examples) and your own programs and as you have them, or you can use the Robot object with setting the escape_pod parameter to True, and passing the robot's ip address: +You can either use the ```anki_vector.configure_pod``` in order to save your authentication into the sdk_config.ini file, and use all the [examples](https://github.com/cyb3rdog/vector-python-sdk/tree/master/examples) and your own programs and as you have them, OR you can use the Robot object setting (or sdk_config) with the ```escape_pod``` parameter to True, and passing just the robot's ip address: ``` with anki_vector.Robot(ip="192.168.0.148", escape_pod=True) as robot: @@ -92,6 +92,27 @@ You can either use the ```anki_vector.configure_pod``` in order to save your aut ``` +### Log Level + +In order to change the log level to other then default value of `INFO`, set the `VECTOR_LOG_LEVEL` enviroment variable: + +Allowed values are: +``` +CRITICAL = 50 +FATAL = CRITICAL +ERROR = 40 +WARNING = 30 +WARN = WARNING +INFO = 20 +DEBUG = 10 +``` + +Example: + +- Windows: ```SET VECTOR_LOG_LEVEL=DEBUG``` +- Lunux: ```VECTOR_LOG_LEVEL="DEBUG"``` + + ### Documentation You can generate a local copy of the SDK documetation by diff --git a/anki_vector/version.py b/anki_vector/version.py index c8fa3ac..ce4b97d 100644 --- a/anki_vector/version.py +++ b/anki_vector/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.7.2.dev4" +__version__ = "0.7.2.dev6" From 98175ee2583bc93be9c80023db15e6cefa93bffe Mon Sep 17 00:00:00 2001 From: cyb3rdog Date: Sun, 12 Dec 2021 10:30:44 +0100 Subject: [PATCH 15/15] Q&D fix for old and custom Vector's software, where CameraConfig does not exist yet --- anki_vector/camera.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/anki_vector/camera.py b/anki_vector/camera.py index af113d8..f0845c6 100644 --- a/anki_vector/camera.py +++ b/anki_vector/camera.py @@ -286,7 +286,10 @@ def __init__(self, robot): def set_config(self, message: protocol.CameraConfigRequest): """Update Vector's camera configuration from the message sent from the Robot """ - self._config = CameraConfig.create_from_message(message) + try: + self._config = CameraConfig.create_from_message(message) + except: + self._config = CameraConfig(0,0,0,0,0,0,0,0,0,0) @connection.on_connection_thread(requires_control=False) async def get_camera_config(self) -> protocol.CameraConfigResponse: @@ -297,8 +300,11 @@ async def get_camera_config(self) -> protocol.CameraConfigResponse: :return: """ - request = protocol.CameraConfigRequest() - return await self.conn.grpc_interface.GetCameraConfig(request) + try: + request = protocol.CameraConfigRequest() + return await self.conn.grpc_interface.GetCameraConfig(request) + except: + pass @property def config(self) -> CameraConfig: