wirepod-vector-sdk-audio 0.9.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- anki_vector/__init__.py +43 -0
- anki_vector/animation.py +272 -0
- anki_vector/annotate.py +590 -0
- anki_vector/audio.py +212 -0
- anki_vector/audio_stream.py +335 -0
- anki_vector/behavior.py +1135 -0
- anki_vector/camera.py +670 -0
- anki_vector/camera_viewer/__init__.py +121 -0
- anki_vector/color.py +88 -0
- anki_vector/configure/__main__.py +331 -0
- anki_vector/connection.py +838 -0
- anki_vector/events.py +420 -0
- anki_vector/exceptions.py +185 -0
- anki_vector/faces.py +819 -0
- anki_vector/lights.py +210 -0
- anki_vector/mdns.py +131 -0
- anki_vector/messaging/__init__.py +45 -0
- anki_vector/messaging/alexa_pb2.py +36 -0
- anki_vector/messaging/alexa_pb2_grpc.py +3 -0
- anki_vector/messaging/behavior_pb2.py +40 -0
- anki_vector/messaging/behavior_pb2_grpc.py +3 -0
- anki_vector/messaging/client.py +33 -0
- anki_vector/messaging/cube_pb2.py +113 -0
- anki_vector/messaging/cube_pb2_grpc.py +3 -0
- anki_vector/messaging/extensions_pb2.py +25 -0
- anki_vector/messaging/extensions_pb2_grpc.py +3 -0
- anki_vector/messaging/external_interface_pb2.py +169 -0
- anki_vector/messaging/external_interface_pb2_grpc.py +1267 -0
- anki_vector/messaging/messages_pb2.py +431 -0
- anki_vector/messaging/messages_pb2_grpc.py +3 -0
- anki_vector/messaging/nav_map_pb2.py +33 -0
- anki_vector/messaging/nav_map_pb2_grpc.py +3 -0
- anki_vector/messaging/protocol.py +33 -0
- anki_vector/messaging/response_status_pb2.py +27 -0
- anki_vector/messaging/response_status_pb2_grpc.py +3 -0
- anki_vector/messaging/settings_pb2.py +72 -0
- anki_vector/messaging/settings_pb2_grpc.py +3 -0
- anki_vector/messaging/shared_pb2.py +54 -0
- anki_vector/messaging/shared_pb2_grpc.py +3 -0
- anki_vector/motors.py +127 -0
- anki_vector/nav_map.py +409 -0
- anki_vector/objects.py +1782 -0
- anki_vector/opengl/__init__.py +103 -0
- anki_vector/opengl/assets/LICENSE.txt +21 -0
- anki_vector/opengl/assets/cube.jpg +0 -0
- anki_vector/opengl/assets/cube.mtl +9 -0
- anki_vector/opengl/assets/cube.obj +1000 -0
- anki_vector/opengl/assets/vector.mtl +67 -0
- anki_vector/opengl/assets/vector.obj +13220 -0
- anki_vector/opengl/opengl.py +864 -0
- anki_vector/opengl/opengl_vector.py +620 -0
- anki_vector/opengl/opengl_viewer.py +689 -0
- anki_vector/photos.py +145 -0
- anki_vector/proximity.py +176 -0
- anki_vector/reserve_control/__main__.py +36 -0
- anki_vector/robot.py +930 -0
- anki_vector/screen.py +201 -0
- anki_vector/status.py +322 -0
- anki_vector/touch.py +119 -0
- anki_vector/user_intent.py +186 -0
- anki_vector/util.py +1132 -0
- anki_vector/version.py +15 -0
- anki_vector/viewer.py +403 -0
- anki_vector/vision.py +202 -0
- anki_vector/world.py +899 -0
- wirepod_vector_sdk_audio-0.9.0.dist-info/METADATA +80 -0
- wirepod_vector_sdk_audio-0.9.0.dist-info/RECORD +71 -0
- wirepod_vector_sdk_audio-0.9.0.dist-info/WHEEL +5 -0
- wirepod_vector_sdk_audio-0.9.0.dist-info/licenses/LICENSE.txt +180 -0
- wirepod_vector_sdk_audio-0.9.0.dist-info/top_level.txt +1 -0
- wirepod_vector_sdk_audio-0.9.0.dist-info/zip-safe +1 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# Copyright (c) 2018 Anki, Inc.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License in the file LICENSE.txt or at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""This module provides the camera viewer's render process.
|
|
16
|
+
|
|
17
|
+
It should be launched in a separate process to allow Vector to run freely while
|
|
18
|
+
the viewer is rendering.
|
|
19
|
+
|
|
20
|
+
It uses Tkinter, a standard Python GUI package.
|
|
21
|
+
It also depends on the Pillow library for image processing.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
import multiprocessing as mp
|
|
25
|
+
import sys
|
|
26
|
+
import tkinter as tk
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
from PIL import ImageTk
|
|
30
|
+
except ImportError:
|
|
31
|
+
sys.exit('Cannot import from PIL: Do `pip3 install --user "wirepod_vector_sdk[3dviewer]"` to install')
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class TkCameraViewer: # pylint: disable=too-few-public-methods
|
|
35
|
+
"""A Tkinter based camera video feed.
|
|
36
|
+
|
|
37
|
+
:param queue: A queue to send frames between the user's main thread and the viewer process.
|
|
38
|
+
:param event: An event to signal that the viewer process has closed.
|
|
39
|
+
:param overlays: Overlays to be drawn on the images of the renderer.
|
|
40
|
+
:param timeout: The time without a new frame before the process will exit.
|
|
41
|
+
:param force_on_top: Specifies whether the window should be forced on top of all others.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
def __init__(self, queue: mp.Queue, event: mp.Event, overlays: list = None, timeout: float = 10.0, force_on_top: bool = True):
|
|
45
|
+
self.tk_root = tk.Tk()
|
|
46
|
+
self.width = None
|
|
47
|
+
self.height = None
|
|
48
|
+
self.queue = queue
|
|
49
|
+
self.event = event
|
|
50
|
+
self.overlays = overlays
|
|
51
|
+
self.timeout = timeout
|
|
52
|
+
self.tk_root.title("Vector Camera Feed")
|
|
53
|
+
self.tk_root.protocol("WM_DELETE_WINDOW", self._delete_window)
|
|
54
|
+
self.tk_root.bind("<Configure>", self._resize_window)
|
|
55
|
+
if force_on_top:
|
|
56
|
+
self.tk_root.wm_attributes("-topmost", 1)
|
|
57
|
+
self.label = tk.Label(self.tk_root, borderwidth=0)
|
|
58
|
+
self.label.pack(fill=tk.BOTH, expand=True)
|
|
59
|
+
|
|
60
|
+
def _delete_window(self) -> None:
|
|
61
|
+
"""Handle window close event."""
|
|
62
|
+
self.event.set()
|
|
63
|
+
self.tk_root.destroy()
|
|
64
|
+
|
|
65
|
+
def _resize_window(self, evt: tk.Event) -> None:
|
|
66
|
+
"""Handle window resize event.
|
|
67
|
+
|
|
68
|
+
:param evt: A Tkinter window event (keyboard, mouse events, etc).
|
|
69
|
+
"""
|
|
70
|
+
self.width = evt.width
|
|
71
|
+
self.height = evt.height
|
|
72
|
+
|
|
73
|
+
def draw_frame(self) -> None:
|
|
74
|
+
"""Display an image on to a Tkinter label widget."""
|
|
75
|
+
try:
|
|
76
|
+
image = self.queue.get(True, timeout=self.timeout)
|
|
77
|
+
except:
|
|
78
|
+
return
|
|
79
|
+
self.width, self.height = image.size
|
|
80
|
+
while image:
|
|
81
|
+
if self.event.is_set():
|
|
82
|
+
break
|
|
83
|
+
if self.overlays:
|
|
84
|
+
for overlay in self.overlays:
|
|
85
|
+
overlay.apply_overlay(image)
|
|
86
|
+
if (self.width, self.height) != image.size:
|
|
87
|
+
image = image.resize((self.width, self.height))
|
|
88
|
+
tk_image = ImageTk.PhotoImage(image)
|
|
89
|
+
self.label.config(image=tk_image)
|
|
90
|
+
self.label.image = tk_image
|
|
91
|
+
self.tk_root.update_idletasks()
|
|
92
|
+
self.tk_root.update()
|
|
93
|
+
try:
|
|
94
|
+
image = self.queue.get(True, timeout=self.timeout)
|
|
95
|
+
except:
|
|
96
|
+
return
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def main(queue: mp.Queue, event: mp.Event, overlays: list = None, timeout: float = 10.0, force_on_top: bool = False) -> None:
|
|
100
|
+
"""Rendering the frames in another process. This allows the UI to have the
|
|
101
|
+
main thread of its process while the user code continues to execute.
|
|
102
|
+
|
|
103
|
+
:param queue: A queue to send frames between the user's main thread and the viewer process.
|
|
104
|
+
:param event: An event to signal that the viewer process has closed.
|
|
105
|
+
:param overlays: Overlays to be drawn on the images of the renderer.
|
|
106
|
+
:param timeout: The time without a new frame before the process will exit.
|
|
107
|
+
:param force_on_top: Specifies whether the window should be forced on top of all others.
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
try:
|
|
111
|
+
tk_viewer = TkCameraViewer(queue, event, overlays, timeout, force_on_top)
|
|
112
|
+
tk_viewer.draw_frame()
|
|
113
|
+
except TimeoutError:
|
|
114
|
+
pass
|
|
115
|
+
except KeyboardInterrupt:
|
|
116
|
+
pass
|
|
117
|
+
finally:
|
|
118
|
+
event.set()
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
__all__ = ['TkCameraViewer', 'main']
|
anki_vector/color.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# Copyright (c) 2018 Anki, Inc.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License in the file LICENSE.txt or at
|
|
6
|
+
#
|
|
7
|
+
# https://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""
|
|
16
|
+
Colors to be used with a light or Vector's screen.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Color:
|
|
21
|
+
"""A Color to be used with a Light or Vector's screen.
|
|
22
|
+
|
|
23
|
+
Either int_color or rgb may be used to specify the actual color.
|
|
24
|
+
Any alpha components (from int_color) are ignored - all colors are fully opaque.
|
|
25
|
+
|
|
26
|
+
:param int_color: A 32 bit value holding the binary RGBA value (where A
|
|
27
|
+
is ignored and forced to be fully opaque).
|
|
28
|
+
:param rgb: A tuple holding the integer values from 0-255 for (reg, green, blue)
|
|
29
|
+
:param name: A name to assign to this color.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(self, int_color: int = None, rgb: tuple = None, name: str = None):
|
|
33
|
+
self.name = name
|
|
34
|
+
self._int_color = 0
|
|
35
|
+
if int_color is not None:
|
|
36
|
+
self._int_color = int_color | 0xff
|
|
37
|
+
elif rgb is not None:
|
|
38
|
+
self._int_color = (rgb[0] << 24) | (rgb[1] << 16) | (rgb[2] << 8) | 0xff
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def int_color(self) -> int:
|
|
42
|
+
"""The encoded integer value of the color."""
|
|
43
|
+
return self._int_color
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def rgb565_bytepair(self):
|
|
47
|
+
"""bytes[]: Two bytes representing an int16 color with rgb565 encoding.
|
|
48
|
+
|
|
49
|
+
This format reflects the robot's Screen color range, and performing this
|
|
50
|
+
conversion will reduce network traffic when sending Screen data.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
red5 = ((self._int_color >> 24) & 0xff) >> 3
|
|
54
|
+
green6 = ((self._int_color >> 16) & 0xff) >> 2
|
|
55
|
+
blue5 = ((self._int_color >> 8) & 0xff) >> 3
|
|
56
|
+
|
|
57
|
+
green3_hi = green6 >> 3
|
|
58
|
+
green3_low = green6 & 0x07
|
|
59
|
+
|
|
60
|
+
int_565_color_lowbyte = (green3_low << 5) | blue5
|
|
61
|
+
int_565_color_highbyte = (red5 << 3) | green3_hi
|
|
62
|
+
|
|
63
|
+
return [int_565_color_highbyte, int_565_color_lowbyte]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
#: :class:`Color`: Green color instance.
|
|
67
|
+
green = Color(name="green", int_color=0x00ff00ff)
|
|
68
|
+
|
|
69
|
+
#: :class:`Color`: Red color instance.
|
|
70
|
+
red = Color(name="red", int_color=0xff0000ff)
|
|
71
|
+
|
|
72
|
+
#: :class:`Color`: Blue color instance.
|
|
73
|
+
blue = Color(name="blue", int_color=0x0000ffff)
|
|
74
|
+
|
|
75
|
+
#: :class:`Color`: Cyan color instance.
|
|
76
|
+
cyan = Color(name="cyan", int_color=0x00ffffff)
|
|
77
|
+
|
|
78
|
+
#: :class:`Color`: Magenta color instance.
|
|
79
|
+
magenta = Color(name="magenta", int_color=0xff00ffff)
|
|
80
|
+
|
|
81
|
+
#: :class:`Color`: Yellow color instance.
|
|
82
|
+
yellow = Color(name="yellow", int_color=0xffff00ff)
|
|
83
|
+
|
|
84
|
+
#: :class:`Color`: White color instance.
|
|
85
|
+
white = Color(name="white", int_color=0xffffffff)
|
|
86
|
+
|
|
87
|
+
#: :class:`Color`: Instance representing no color (i.e., lights off).
|
|
88
|
+
off = Color(name="off")
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
***Anki Vector Python SDK Setup***
|
|
5
|
+
|
|
6
|
+
Vector requires all requests be authorized by an authenticated Anki user.
|
|
7
|
+
|
|
8
|
+
This script will enable this device to authenticate with your Vector
|
|
9
|
+
robot for use with a Vector Python SDK program.
|
|
10
|
+
|
|
11
|
+
Vector must be powered on and connected on the same network as your
|
|
12
|
+
computer. By running this script, you will be asked to provide your
|
|
13
|
+
Anki account credentials, and the script will download an authentication
|
|
14
|
+
token and cert that will grant you access to the robot and his
|
|
15
|
+
capabilities (such as camera and audio) as well as data stored on the
|
|
16
|
+
robot (such as faces and photos).
|
|
17
|
+
|
|
18
|
+
See the README for more information.
|
|
19
|
+
|
|
20
|
+
Use of Vector and the Vector SDK is subject to Anki's Privacy Policy and Terms and Conditions.
|
|
21
|
+
|
|
22
|
+
https://www.anki.com/en-us/company/privacy
|
|
23
|
+
https://www.anki.com/en-us/company/terms-and-conditions
|
|
24
|
+
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
import argparse
|
|
28
|
+
import configparser
|
|
29
|
+
from getpass import getpass
|
|
30
|
+
import json
|
|
31
|
+
import os
|
|
32
|
+
from pathlib import Path
|
|
33
|
+
import platform
|
|
34
|
+
import re
|
|
35
|
+
import socket
|
|
36
|
+
import sys
|
|
37
|
+
|
|
38
|
+
from cryptography import x509
|
|
39
|
+
from cryptography.hazmat.backends import default_backend
|
|
40
|
+
import grpc
|
|
41
|
+
import requests
|
|
42
|
+
try:
|
|
43
|
+
from termcolor import colored # pylint: disable=import-error
|
|
44
|
+
except: # pylint: disable=bare-except
|
|
45
|
+
def colored(text, color=None, on_color=None, attrs=None): # pylint: disable=unused-argument
|
|
46
|
+
return text
|
|
47
|
+
|
|
48
|
+
import anki_vector
|
|
49
|
+
from anki_vector import messaging
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class ApiHandler:
|
|
53
|
+
def __init__(self, headers: dict, url: str):
|
|
54
|
+
self._headers = headers
|
|
55
|
+
self._url = url
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def headers(self):
|
|
59
|
+
return self._headers
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def url(self):
|
|
63
|
+
return self._url
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class Api:
|
|
67
|
+
def __init__(self):
|
|
68
|
+
self._handler = ApiHandler(
|
|
69
|
+
headers={
|
|
70
|
+
'User-Agent': 'Vector-sdk/{} {}/{}'.format(anki_vector.__version__,
|
|
71
|
+
platform.python_implementation(),
|
|
72
|
+
platform.python_version()),
|
|
73
|
+
|
|
74
|
+
'Anki-App-Key': 'aung2ieCho3aiph7Een3Ei'
|
|
75
|
+
},
|
|
76
|
+
url='https://accounts.api.anki.com/1/sessions'
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def name(self):
|
|
81
|
+
return "Anki Cloud"
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def handler(self):
|
|
85
|
+
return self._handler
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def get_serial(serial=None):
|
|
89
|
+
if not serial:
|
|
90
|
+
serial = os.environ.get('ANKI_ROBOT_SERIAL')
|
|
91
|
+
if not serial:
|
|
92
|
+
print("\n\nPlease find your robot serial number (ex. 00e20100) located on the underside of Vector, or accessible from Vector's debug screen.")
|
|
93
|
+
serial = input('Enter robot serial number: ')
|
|
94
|
+
else:
|
|
95
|
+
print("Found robot serial number in environment variable '{}'".format(colored("ANKI_ROBOT_SERIAL", "green")))
|
|
96
|
+
serial = serial.lower()
|
|
97
|
+
print("Using robot serial number: {}".format(colored(serial, "cyan")))
|
|
98
|
+
return serial
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def get_cert(serial=None):
|
|
102
|
+
print("\n\nEnter the IP address and webserver port of your wire-pod instance (ex. 192.168.1.50:8080) (:8080 is the default port)\nLeave this blank and press enter if you want this script to attempt to automatically connect to your wire-pod instance via escapepod.local.")
|
|
103
|
+
podip = input("Enter wire-pod ip: ")
|
|
104
|
+
if podip == "":
|
|
105
|
+
podip = "escapepod.local:8080"
|
|
106
|
+
serial = get_serial(serial)
|
|
107
|
+
print("\nDownloading Vector certificate from wire-pod...", end="")
|
|
108
|
+
sys.stdout.flush()
|
|
109
|
+
r = requests.get('http://{}/session-certs/{}'.format(podip, serial))
|
|
110
|
+
if r.status_code != 200:
|
|
111
|
+
print(colored(" ERROR", "red"))
|
|
112
|
+
sys.exit(r.content)
|
|
113
|
+
print(colored(" DONE", "green"))
|
|
114
|
+
cert = r.content
|
|
115
|
+
return cert, serial
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def user_authentication(session_id: bytes, cert: bytes, ip: str, name: str) -> str:
|
|
119
|
+
# Pin the robot certificate for opening the channel
|
|
120
|
+
creds = grpc.ssl_channel_credentials(root_certificates=cert)
|
|
121
|
+
|
|
122
|
+
print("Attempting to download guid from {} at {}:443...".format(colored(name, "cyan"), colored(ip, "cyan")), end="")
|
|
123
|
+
sys.stdout.flush()
|
|
124
|
+
channel = grpc.secure_channel("{}:443".format(ip), creds,
|
|
125
|
+
options=(("grpc.ssl_target_name_override", name,),))
|
|
126
|
+
|
|
127
|
+
# Verify the connection to Vector is able to be established (client-side)
|
|
128
|
+
try:
|
|
129
|
+
# Explicitly grab _channel._channel to test the underlying grpc channel directly
|
|
130
|
+
grpc.channel_ready_future(channel).result(timeout=15)
|
|
131
|
+
except grpc.FutureTimeoutError:
|
|
132
|
+
print(colored(" ERROR", "red"))
|
|
133
|
+
sys.exit("\nUnable to connect to Vector\n"
|
|
134
|
+
"Please be sure to connect via the Vector companion app first, and connect your computer to the same network as your Vector.")
|
|
135
|
+
|
|
136
|
+
try:
|
|
137
|
+
interface = messaging.client.ExternalInterfaceStub(channel)
|
|
138
|
+
request = messaging.protocol.UserAuthenticationRequest(
|
|
139
|
+
user_session_id=session_id.encode('utf-8'),
|
|
140
|
+
client_name=socket.gethostname().encode('utf-8'))
|
|
141
|
+
response = interface.UserAuthentication(request)
|
|
142
|
+
if response.code != messaging.protocol.UserAuthenticationResponse.AUTHORIZED: # pylint: disable=no-member
|
|
143
|
+
print(colored(" ERROR", "red"))
|
|
144
|
+
sys.exit("\nFailed to authorize request:\n"
|
|
145
|
+
"Please be sure to first set up Vector using the companion app.")
|
|
146
|
+
except grpc.RpcError as e:
|
|
147
|
+
print(colored(" ERROR", "red"))
|
|
148
|
+
sys.exit("\nFailed to authorize request:\n"
|
|
149
|
+
"An unknown error occurred '{}'".format(e))
|
|
150
|
+
|
|
151
|
+
print(colored(" DONE\n", "green"))
|
|
152
|
+
return response.client_token_guid
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def get_session_token(api, username=None):
|
|
156
|
+
print("Enter your email and password. Make sure to use the same account that was used to set up your Vector.")
|
|
157
|
+
if not username:
|
|
158
|
+
username = input("Enter Email: ")
|
|
159
|
+
else:
|
|
160
|
+
print("Using email from command line: {}".format(colored(username, "cyan")))
|
|
161
|
+
password = getpass("Enter Password: ")
|
|
162
|
+
payload = {'username': username, 'password': password}
|
|
163
|
+
|
|
164
|
+
print("\nAuthenticating with {}...".format(api.name), end="")
|
|
165
|
+
sys.stdout.flush()
|
|
166
|
+
r = requests.post(api.handler.url, data=payload, headers=api.handler.headers)
|
|
167
|
+
if r.status_code != 200:
|
|
168
|
+
print(colored(" ERROR", "red"))
|
|
169
|
+
sys.exit(r.content)
|
|
170
|
+
print(colored(" DONE\n", "green"))
|
|
171
|
+
return json.loads(r.content)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def standardize_name(robot_name):
|
|
175
|
+
# Extend the name if not enough is provided
|
|
176
|
+
if len(robot_name) == 4:
|
|
177
|
+
robot_name = "Vector-{}".format(robot_name.upper())
|
|
178
|
+
# Fix possible capitalization and space/dash/etc.
|
|
179
|
+
if re.match("[Vv]ector.[A-Za-z0-9]{4}", robot_name):
|
|
180
|
+
robot_name = "V{}-{}".format(robot_name[1:-5], robot_name[-4:].upper())
|
|
181
|
+
# Check that the end is valid
|
|
182
|
+
if re.match("Vector-[A-Z0-9]{4}", robot_name):
|
|
183
|
+
return robot_name
|
|
184
|
+
print(colored(" ERROR", "red"))
|
|
185
|
+
sys.exit("Invalid robot name. Please match the format exactly. Example: Vector-A1B2")
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def get_name_and_ip(robot_name=None, ip=None):
|
|
189
|
+
if not robot_name:
|
|
190
|
+
robot_name = os.environ.get('VECTOR_ROBOT_NAME')
|
|
191
|
+
if not robot_name:
|
|
192
|
+
print("\n\nFind your robot name (ex. Vector-A1B2) by placing Vector on the charger and double-clicking Vector's backpack button.")
|
|
193
|
+
robot_name = input("Enter robot name: ")
|
|
194
|
+
else:
|
|
195
|
+
print("Found robot name in environment variable '{}'".format(colored("VECTOR_ROBOT_NAME", "green")))
|
|
196
|
+
robot_name = standardize_name(robot_name)
|
|
197
|
+
print("Using robot name: {}".format(colored(robot_name, "cyan")))
|
|
198
|
+
if not ip:
|
|
199
|
+
ip = os.environ.get('ANKI_ROBOT_HOST')
|
|
200
|
+
if not ip:
|
|
201
|
+
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"
|
|
202
|
+
"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")))
|
|
203
|
+
ip = input("Enter robot ip: ")
|
|
204
|
+
else:
|
|
205
|
+
print("Found robot ip address in environment variable '{}'".format(colored("ANKI_ROBOT_HOST", "green")))
|
|
206
|
+
print("Using IP: {}".format(colored(ip, "cyan")))
|
|
207
|
+
return robot_name, ip
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def save_cert(cert, name, serial, anki_dir):
|
|
211
|
+
"""Write Vector's certificate to a file located in the user's home directory"""
|
|
212
|
+
os.makedirs(str(anki_dir), exist_ok=True)
|
|
213
|
+
cert_file = str(anki_dir / "{name}-{serial}.cert".format(name=name, serial=serial))
|
|
214
|
+
print("Writing certificate file to '{}'...\n".format(colored(cert_file, "cyan")))
|
|
215
|
+
with os.fdopen(os.open(cert_file, os.O_WRONLY | os.O_CREAT, 0o600), 'wb') as f:
|
|
216
|
+
f.write(cert)
|
|
217
|
+
return cert_file
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def validate_cert_name(cert_file, robot_name):
|
|
221
|
+
"""Validate the name on Vector's certificate against the user-provided name"""
|
|
222
|
+
with open(cert_file, "rb") as f:
|
|
223
|
+
cert_file = f.read()
|
|
224
|
+
cert = x509.load_pem_x509_certificate(cert_file, default_backend())
|
|
225
|
+
for fields in cert.subject:
|
|
226
|
+
current = str(fields.oid)
|
|
227
|
+
if "commonName" in current:
|
|
228
|
+
common_name = fields.value
|
|
229
|
+
if common_name != robot_name:
|
|
230
|
+
print(colored(" ERROR", "red"))
|
|
231
|
+
sys.exit("The name of the certificate ({}) does not match the name provided ({}).\n"
|
|
232
|
+
"Please verify the name, and try again.".format(common_name, robot_name))
|
|
233
|
+
else:
|
|
234
|
+
return
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def write_config(serial, cert_file=None, ip=None, name=None, guid=None, clear=True):
|
|
238
|
+
home = Path.home()
|
|
239
|
+
config_file = str(home / ".anki_vector" / "sdk_config.ini")
|
|
240
|
+
print("Writing config file to '{}'...".format(colored(config_file, "cyan")))
|
|
241
|
+
|
|
242
|
+
config = configparser.ConfigParser(strict=False)
|
|
243
|
+
|
|
244
|
+
try:
|
|
245
|
+
config.read(config_file)
|
|
246
|
+
except configparser.ParsingError:
|
|
247
|
+
if os.path.exists(config_file):
|
|
248
|
+
os.rename(config_file, config_file + "-error")
|
|
249
|
+
if clear:
|
|
250
|
+
config[serial] = {}
|
|
251
|
+
if cert_file:
|
|
252
|
+
config[serial]["cert"] = cert_file
|
|
253
|
+
if ip:
|
|
254
|
+
config[serial]["ip"] = ip
|
|
255
|
+
if name:
|
|
256
|
+
config[serial]["name"] = name
|
|
257
|
+
if guid:
|
|
258
|
+
config[serial]["guid"] = guid.decode("utf-8")
|
|
259
|
+
temp_file = config_file + "-temp"
|
|
260
|
+
if os.path.exists(config_file):
|
|
261
|
+
os.rename(config_file, temp_file)
|
|
262
|
+
try:
|
|
263
|
+
with os.fdopen(os.open(config_file, os.O_WRONLY | os.O_CREAT, 0o600), 'w') as f:
|
|
264
|
+
config.write(f)
|
|
265
|
+
except Exception as e:
|
|
266
|
+
if os.path.exists(temp_file):
|
|
267
|
+
os.rename(temp_file, config_file)
|
|
268
|
+
raise e
|
|
269
|
+
else:
|
|
270
|
+
if os.path.exists(temp_file):
|
|
271
|
+
os.remove(temp_file)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def main(api):
|
|
275
|
+
parser = argparse.ArgumentParser(description=("Vector requires all requests be authorized by an authenticated Anki user. "
|
|
276
|
+
"This script will enable this device to authenticate with your Vector "
|
|
277
|
+
"robot for use with a Vector Python SDK program."),
|
|
278
|
+
epilog=("See the README for more information. "
|
|
279
|
+
"Use of Vector and the Vector SDK is subject to Anki's Privacy Policy and Terms and Conditions. "
|
|
280
|
+
"https://www.anki.com/en-us/company/privacy and "
|
|
281
|
+
"https://www.anki.com/en-us/company/terms-and-conditions"))
|
|
282
|
+
parser.add_argument("-e", "--email", help="The email used by your Anki account.")
|
|
283
|
+
parser.add_argument("-i", "--ip", help=("Your robot ip address (ex. 192.168.42.42). "
|
|
284
|
+
"It may be found by placing Vector on the charger, "
|
|
285
|
+
"double-clicking Vector's backpack button, "
|
|
286
|
+
"then raising and lowering his arms. "
|
|
287
|
+
"If you see {} on his face, "
|
|
288
|
+
"reconnect Vector to your WiFi using the Vector Companion App.".format(colored("XX.XX.XX.XX", "red"))))
|
|
289
|
+
parser.add_argument("-n", "--name", help=("Your robot name (ex. Vector-A1B2). "
|
|
290
|
+
"It may be found by placing Vector on the charger and double-clicking Vector's backpack button."))
|
|
291
|
+
parser.add_argument("-s", "--serial", help=("Your robot serial number (ex. 00e20100). "
|
|
292
|
+
"It is located on the underside of Vector, or accessible from Vector's debug screen."))
|
|
293
|
+
parser.add_argument("-u", "--update", dest="new_ip", help=("Update the stored ip for Vector. This makes it easier to transfer between networks."))
|
|
294
|
+
args = parser.parse_args()
|
|
295
|
+
|
|
296
|
+
if args.new_ip:
|
|
297
|
+
serial = get_serial(args.serial)
|
|
298
|
+
write_config(serial, ip=args.new_ip, clear=False)
|
|
299
|
+
print(colored("\nIP Updated!", "green"))
|
|
300
|
+
sys.exit()
|
|
301
|
+
|
|
302
|
+
print(__doc__)
|
|
303
|
+
|
|
304
|
+
valid = ["y", "Y", "yes", "YES"]
|
|
305
|
+
environ = input("Do you wish to proceed? (y/n) ")
|
|
306
|
+
if environ not in valid:
|
|
307
|
+
sys.exit("Stopping...")
|
|
308
|
+
|
|
309
|
+
name, ip = get_name_and_ip(args.name, args.ip)
|
|
310
|
+
cert, serial = get_cert(args.serial)
|
|
311
|
+
|
|
312
|
+
home = Path.home()
|
|
313
|
+
anki_dir = home / ".anki_vector"
|
|
314
|
+
|
|
315
|
+
cert_file = save_cert(cert, name, serial, anki_dir)
|
|
316
|
+
validate_cert_name(cert_file, name)
|
|
317
|
+
|
|
318
|
+
# token = get_session_token(api, args.email)
|
|
319
|
+
# if not token.get("session"):
|
|
320
|
+
# sys.exit("Session error: {}".format(token))
|
|
321
|
+
token = "2vMhFgktH3Jrbemm2WHkfGN"
|
|
322
|
+
|
|
323
|
+
guid = user_authentication(token, cert, ip, name)
|
|
324
|
+
|
|
325
|
+
# Store credentials in the .anki_vector directory's sdk_config.ini file
|
|
326
|
+
write_config(serial, cert_file, ip, name, guid)
|
|
327
|
+
print(colored("\nSUCCESS!", "green"))
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
if __name__ == "__main__":
|
|
331
|
+
main(Api())
|