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,689 @@
|
|
|
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
|
+
"""This module provides a 3D visualizer for Vector's world state and a 2D camera window.
|
|
16
|
+
|
|
17
|
+
It uses PyOpenGL, a Python OpenGL 3D graphics library which is available on most
|
|
18
|
+
platforms. It also depends on the Pillow library for image processing.
|
|
19
|
+
|
|
20
|
+
Example:
|
|
21
|
+
.. testcode::
|
|
22
|
+
|
|
23
|
+
import time
|
|
24
|
+
|
|
25
|
+
import anki_vector
|
|
26
|
+
|
|
27
|
+
with anki_vector.Robot(show_viewer=True,
|
|
28
|
+
show_3d_viewer=True,
|
|
29
|
+
enable_face_detection=True,
|
|
30
|
+
enable_custom_object_detection=True,
|
|
31
|
+
enable_nav_map_feed=True) as robot:
|
|
32
|
+
time.sleep(10)
|
|
33
|
+
|
|
34
|
+
Warning:
|
|
35
|
+
This package requires Python to have the PyOpenGL package installed, along
|
|
36
|
+
with an implementation of GLUT (OpenGL Utility Toolkit).
|
|
37
|
+
|
|
38
|
+
To install the Python packages on Mac and Linux do ``python3 -m pip install --user "wirepod_vector_sdk[3dviewer]"``
|
|
39
|
+
|
|
40
|
+
To install the Python packages on Windows do ``py -3 -m pip install --user "wirepod_vector_sdk[3dviewer]"``
|
|
41
|
+
|
|
42
|
+
On Windows and Linux you must also install freeglut (macOS / OSX has one
|
|
43
|
+
preinstalled).
|
|
44
|
+
|
|
45
|
+
On Linux: ``sudo apt-get install freeglut3``
|
|
46
|
+
|
|
47
|
+
On Windows: Go to http://freeglut.sourceforge.net/ to get a ``freeglut.dll``
|
|
48
|
+
file. It's included in any of the `Windows binaries` downloads. Place the DLL
|
|
49
|
+
next to your Python script, or install it somewhere in your PATH to allow any
|
|
50
|
+
script to use it."
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
# __all__ should order by constants, event classes, other classes, functions.
|
|
54
|
+
__all__ = ['OpenGLViewer']
|
|
55
|
+
|
|
56
|
+
import math
|
|
57
|
+
import multiprocessing as mp
|
|
58
|
+
import sys
|
|
59
|
+
from typing import List
|
|
60
|
+
|
|
61
|
+
from anki_vector import nav_map, util
|
|
62
|
+
from . import opengl, opengl_vector
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
from OpenGL.GL import (GL_FILL,
|
|
67
|
+
GL_FRONT_AND_BACK,
|
|
68
|
+
GL_LIGHTING, GL_NORMALIZE,
|
|
69
|
+
GL_TEXTURE_2D,
|
|
70
|
+
glBindTexture, glColor3f, glDisable, glEnable,
|
|
71
|
+
glMultMatrixf, glPolygonMode, glPopMatrix, glPushMatrix,
|
|
72
|
+
glScalef, glWindowPos2f)
|
|
73
|
+
from OpenGL.GLUT import (ctypes,
|
|
74
|
+
GLUT_ACTIVE_ALT, GLUT_ACTIVE_CTRL, GLUT_ACTIVE_SHIFT, GLUT_BITMAP_9_BY_15,
|
|
75
|
+
GLUT_DOWN, GLUT_LEFT_BUTTON, GLUT_RIGHT_BUTTON, GLUT_VISIBLE,
|
|
76
|
+
glutBitmapCharacter, glutCheckLoop, glutGetModifiers, glutIdleFunc,
|
|
77
|
+
glutKeyboardFunc, glutKeyboardUpFunc, glutMainLoop, glutMouseFunc, glutMotionFunc, glutPassiveMotionFunc,
|
|
78
|
+
glutPostRedisplay, glutSpecialFunc, glutSpecialUpFunc, glutVisibilityFunc)
|
|
79
|
+
from OpenGL.error import NullFunctionError
|
|
80
|
+
|
|
81
|
+
except ImportError as import_exc:
|
|
82
|
+
opengl.raise_opengl_or_pillow_import_error(import_exc)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
# Constants
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class _RobotControlIntents(): # pylint: disable=too-few-public-methods
|
|
89
|
+
"""Input intents for controlling the robot.
|
|
90
|
+
|
|
91
|
+
These are sent from the OpenGL process, and consumed by the main process for
|
|
92
|
+
issuing movement commands on Vector (to provide a remote-control interface).
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
def __init__(self, left_wheel_speed=0.0, right_wheel_speed=0.0,
|
|
96
|
+
lift_speed=0.0, head_speed=0.0, connect_to_light_block=False):
|
|
97
|
+
self.left_wheel_speed = left_wheel_speed
|
|
98
|
+
self.right_wheel_speed = right_wheel_speed
|
|
99
|
+
self.lift_speed = lift_speed
|
|
100
|
+
self.head_speed = head_speed
|
|
101
|
+
self.connect_to_light_block = connect_to_light_block
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _draw_text(font, input_str, x, y, line_height=16, r=1.0, g=1.0, b=1.0):
|
|
105
|
+
"""Render text based on window position. The origin is in the bottom-left."""
|
|
106
|
+
glColor3f(r, g, b)
|
|
107
|
+
glWindowPos2f(x, y)
|
|
108
|
+
input_list = input_str.split('\n')
|
|
109
|
+
y = y + (line_height * (len(input_list) - 1))
|
|
110
|
+
for line in input_list:
|
|
111
|
+
glWindowPos2f(x, y)
|
|
112
|
+
y -= line_height
|
|
113
|
+
for ch in line:
|
|
114
|
+
glutBitmapCharacter(font, ctypes.c_int(ord(ch)))
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _glut_install_instructions():
|
|
118
|
+
if sys.platform.startswith('linux'):
|
|
119
|
+
return "Install freeglut: `sudo apt-get install freeglut3`"
|
|
120
|
+
if sys.platform.startswith('darwin'):
|
|
121
|
+
return "GLUT should already be installed by default on macOS!"
|
|
122
|
+
if sys.platform in ('win32', 'cygwin'):
|
|
123
|
+
return "Install freeglut: You can download it from http://freeglut.sourceforge.net/ \n"\
|
|
124
|
+
"You just need the `freeglut.dll` file, from any of the 'Windows binaries' downloads. "\
|
|
125
|
+
"Place the DLL next to your Python script, or install it somewhere in your PATH "\
|
|
126
|
+
"to allow any script to use it."
|
|
127
|
+
return "(Instructions unknown for platform %s)" % sys.platform
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class _OpenGLViewController():
|
|
131
|
+
"""Controller that registers for keyboard and mouse input through GLUT, and uses them to update
|
|
132
|
+
the camera and listen for a shutdown cue.
|
|
133
|
+
|
|
134
|
+
:param shutdown_delegate: Function to call when we want to exit the host OpenGLViewer.
|
|
135
|
+
:param camera: The camera object for the controller to mutate.
|
|
136
|
+
:param input_intent_queue: Sends key commands from the 3D viewer process to the main process.
|
|
137
|
+
:type input_intent_queue: multiprocessing.Queue
|
|
138
|
+
:param viewer: A reference to the owning OpenGLViewer.
|
|
139
|
+
:type viewer: OpenGLViewer
|
|
140
|
+
"""
|
|
141
|
+
|
|
142
|
+
def __init__(self, shutdown_delegate: callable, camera: opengl.Camera, input_intent_queue: mp.Queue, viewer):
|
|
143
|
+
|
|
144
|
+
self._logger = util.get_class_logger(__name__, self)
|
|
145
|
+
self._input_intent_queue = input_intent_queue
|
|
146
|
+
self._last_robot_control_intents = _RobotControlIntents()
|
|
147
|
+
self._is_keyboard_control_enabled = False
|
|
148
|
+
|
|
149
|
+
# Keyboard
|
|
150
|
+
self._is_key_pressed = {}
|
|
151
|
+
self._is_alt_down = False
|
|
152
|
+
self._is_ctrl_down = False
|
|
153
|
+
self._is_shift_down = False
|
|
154
|
+
|
|
155
|
+
# Mouse
|
|
156
|
+
self._is_mouse_down = {}
|
|
157
|
+
self._mouse_pos = None # type: util.Vector2
|
|
158
|
+
|
|
159
|
+
self._shutdown_delegate = shutdown_delegate
|
|
160
|
+
|
|
161
|
+
self._last_robot_position = None
|
|
162
|
+
|
|
163
|
+
self._camera = camera
|
|
164
|
+
|
|
165
|
+
self._opengl_viewer = viewer
|
|
166
|
+
|
|
167
|
+
#### Public Properties ####
|
|
168
|
+
|
|
169
|
+
@property
|
|
170
|
+
def last_robot_position(self):
|
|
171
|
+
return self._last_robot_position
|
|
172
|
+
|
|
173
|
+
@last_robot_position.setter
|
|
174
|
+
def last_robot_position(self, last_robot_position):
|
|
175
|
+
self._last_robot_position = last_robot_position
|
|
176
|
+
|
|
177
|
+
#### Public Methods ####
|
|
178
|
+
|
|
179
|
+
def initialize(self):
|
|
180
|
+
"""Sets up the OpenGL window and binds input callbacks to it
|
|
181
|
+
"""
|
|
182
|
+
|
|
183
|
+
glutKeyboardFunc(self._on_key_down)
|
|
184
|
+
glutSpecialFunc(self._on_special_key_down)
|
|
185
|
+
|
|
186
|
+
# [Keyboard/Special]Up methods aren't supported on some old GLUT implementations
|
|
187
|
+
has_keyboard_up = False
|
|
188
|
+
has_special_up = False
|
|
189
|
+
try:
|
|
190
|
+
if bool(glutKeyboardUpFunc):
|
|
191
|
+
glutKeyboardUpFunc(self._on_key_up)
|
|
192
|
+
has_keyboard_up = True
|
|
193
|
+
if bool(glutSpecialUpFunc):
|
|
194
|
+
glutSpecialUpFunc(self._on_special_key_up)
|
|
195
|
+
has_special_up = True
|
|
196
|
+
except NullFunctionError:
|
|
197
|
+
# Methods aren't available on this GLUT version
|
|
198
|
+
pass
|
|
199
|
+
|
|
200
|
+
if not has_keyboard_up or not has_special_up:
|
|
201
|
+
# Warn on old GLUT implementations that don't implement much of the interface.
|
|
202
|
+
self._logger.warning("Warning: Old GLUT implementation detected - keyboard remote control of Vector disabled."
|
|
203
|
+
"We recommend installing freeglut. %s", _glut_install_instructions())
|
|
204
|
+
self._is_keyboard_control_enabled = False
|
|
205
|
+
else:
|
|
206
|
+
self._is_keyboard_control_enabled = True
|
|
207
|
+
|
|
208
|
+
try:
|
|
209
|
+
GLUT_BITMAP_9_BY_15
|
|
210
|
+
except NameError:
|
|
211
|
+
self._logger.warning("Warning: GLUT font not detected. Help message will be unavailable.")
|
|
212
|
+
|
|
213
|
+
glutMouseFunc(self._on_mouse_button)
|
|
214
|
+
glutMotionFunc(self._on_mouse_move)
|
|
215
|
+
glutPassiveMotionFunc(self._on_mouse_move)
|
|
216
|
+
|
|
217
|
+
glutIdleFunc(self._idle)
|
|
218
|
+
glutVisibilityFunc(self._visible)
|
|
219
|
+
|
|
220
|
+
#### Private Methods ####
|
|
221
|
+
|
|
222
|
+
def _update_modifier_keys(self):
|
|
223
|
+
"""Updates alt, ctrl, and shift states.
|
|
224
|
+
"""
|
|
225
|
+
modifiers = glutGetModifiers()
|
|
226
|
+
self._is_alt_down = (modifiers & GLUT_ACTIVE_ALT != 0)
|
|
227
|
+
self._is_ctrl_down = (modifiers & GLUT_ACTIVE_CTRL != 0)
|
|
228
|
+
self._is_shift_down = (modifiers & GLUT_ACTIVE_SHIFT != 0)
|
|
229
|
+
|
|
230
|
+
def _key_byte_to_lower(self, key): # pylint: disable=no-self-use
|
|
231
|
+
"""Convert bytes-object (representing keyboard character) to lowercase equivalent.
|
|
232
|
+
"""
|
|
233
|
+
if b'A' <= key <= b'Z':
|
|
234
|
+
lowercase_key = ord(key) - ord(b'A') + ord(b'a')
|
|
235
|
+
lowercase_key = bytes([lowercase_key])
|
|
236
|
+
return lowercase_key
|
|
237
|
+
return key
|
|
238
|
+
|
|
239
|
+
def _on_key_up(self, key, x, y): # pylint: disable=unused-argument
|
|
240
|
+
"""Called by GLUT when a standard keyboard key is released.
|
|
241
|
+
|
|
242
|
+
:param key: which key was released.
|
|
243
|
+
:param x: the x coordinate of the mouse cursor.
|
|
244
|
+
:param y: the y coordinate of the mouse cursor.
|
|
245
|
+
"""
|
|
246
|
+
key = self._key_byte_to_lower(key)
|
|
247
|
+
self._update_modifier_keys()
|
|
248
|
+
self._is_key_pressed[key] = False
|
|
249
|
+
|
|
250
|
+
def _on_key_down(self, key, x, y): # pylint: disable=unused-argument
|
|
251
|
+
"""Called by GLUT when a standard keyboard key is pressed.
|
|
252
|
+
|
|
253
|
+
:param key: which key was released.
|
|
254
|
+
:param x: the x coordinate of the mouse cursor.
|
|
255
|
+
:param y: the y coordinate of the mouse cursor.
|
|
256
|
+
"""
|
|
257
|
+
key = self._key_byte_to_lower(key)
|
|
258
|
+
self._update_modifier_keys()
|
|
259
|
+
self._is_key_pressed[key] = True
|
|
260
|
+
|
|
261
|
+
if ord(key) == 9: # Tab
|
|
262
|
+
# Set Look-At point to current robot position
|
|
263
|
+
if self._last_robot_position is not None:
|
|
264
|
+
self._camera.look_at = self._last_robot_position
|
|
265
|
+
elif ord(key) == 27: # Escape key
|
|
266
|
+
self._shutdown_delegate()
|
|
267
|
+
elif ord(key) == 72 or ord(key) == 104: # H key
|
|
268
|
+
self._opengl_viewer.show_controls = not self._opengl_viewer.show_controls
|
|
269
|
+
|
|
270
|
+
def _on_special_key_up(self, key, x, y): # pylint: disable=unused-argument
|
|
271
|
+
"""Called by GLUT when a special key is released.
|
|
272
|
+
|
|
273
|
+
:param key: which key was released.
|
|
274
|
+
:param x: the x coordinate of the mouse cursor.
|
|
275
|
+
:param y: the y coordinate of the mouse cursor.
|
|
276
|
+
"""
|
|
277
|
+
self._update_modifier_keys()
|
|
278
|
+
|
|
279
|
+
def _on_special_key_down(self, key, x, y): # pylint: disable=unused-argument
|
|
280
|
+
"""Called by GLUT when a special key is pressed.
|
|
281
|
+
|
|
282
|
+
:param key: which key was pressed.
|
|
283
|
+
:param x: the x coordinate of the mouse cursor.
|
|
284
|
+
:param y: the y coordinate of the mouse cursor.
|
|
285
|
+
"""
|
|
286
|
+
self._update_modifier_keys()
|
|
287
|
+
|
|
288
|
+
def _on_mouse_button(self, button, state, x, y):
|
|
289
|
+
"""Called by GLUT when a mouse button is pressed.
|
|
290
|
+
|
|
291
|
+
:param button: which button was pressed.
|
|
292
|
+
:param state: the current state of the button.
|
|
293
|
+
:param x: the x coordinate of the mouse cursor.
|
|
294
|
+
:param y: the y coordinate of the mouse cursor.
|
|
295
|
+
"""
|
|
296
|
+
# Don't update modifier keys- reading modifier keys is unreliable
|
|
297
|
+
# from _on_mouse_button (for LMB down/up), only SHIFT key seems to read there
|
|
298
|
+
# self._update_modifier_keys()
|
|
299
|
+
is_down = (state == GLUT_DOWN)
|
|
300
|
+
self._is_mouse_down[button] = is_down
|
|
301
|
+
self._mouse_pos = util.Vector2(x, y)
|
|
302
|
+
|
|
303
|
+
def _on_mouse_move(self, x, y):
|
|
304
|
+
"""Handles mouse movement.
|
|
305
|
+
|
|
306
|
+
:param x: the x coordinate of the mouse cursor.
|
|
307
|
+
:param y: the y coordinate of the mouse cursor.
|
|
308
|
+
"""
|
|
309
|
+
|
|
310
|
+
# is_active is True if this is not passive (i.e. a mouse button was down)
|
|
311
|
+
last_mouse_pos = self._mouse_pos
|
|
312
|
+
self._mouse_pos = util.Vector2(x, y)
|
|
313
|
+
if last_mouse_pos is None:
|
|
314
|
+
# First mouse update - ignore (we need a delta of mouse positions)
|
|
315
|
+
return
|
|
316
|
+
|
|
317
|
+
left_button = self._is_mouse_down.get(GLUT_LEFT_BUTTON, False)
|
|
318
|
+
# For laptop and other 1-button mouse users, treat 'x' key as a right mouse button too
|
|
319
|
+
right_button = (self._is_mouse_down.get(GLUT_RIGHT_BUTTON, False)
|
|
320
|
+
or self._is_key_pressed.get(b'x', False))
|
|
321
|
+
|
|
322
|
+
MOUSE_SPEED_SCALAR = 1.0 # general scalar for all mouse movement sensitivity
|
|
323
|
+
MOUSE_ROTATE_SCALAR = 0.025 # additional scalar for rotation sensitivity
|
|
324
|
+
mouse_delta = (self._mouse_pos - last_mouse_pos) * MOUSE_SPEED_SCALAR
|
|
325
|
+
|
|
326
|
+
if left_button and right_button:
|
|
327
|
+
# Move up/down
|
|
328
|
+
self._camera.move(up_amount=-mouse_delta.y)
|
|
329
|
+
elif right_button:
|
|
330
|
+
# Move forward/back and left/right
|
|
331
|
+
self._camera.move(forward_amount=mouse_delta.y, right_amount=mouse_delta.x)
|
|
332
|
+
elif left_button:
|
|
333
|
+
if self._is_key_pressed.get(b'z', False):
|
|
334
|
+
# Zoom in/out
|
|
335
|
+
self._camera.zoom(mouse_delta.y)
|
|
336
|
+
else:
|
|
337
|
+
self._camera.turn(mouse_delta.x * MOUSE_ROTATE_SCALAR, mouse_delta.y * MOUSE_ROTATE_SCALAR)
|
|
338
|
+
|
|
339
|
+
def _update_intents_for_robot(self):
|
|
340
|
+
# Update driving intents based on current input, and pass to SDK thread
|
|
341
|
+
# so that it can pass the input onto the robot.
|
|
342
|
+
def get_intent_direction(key1, key2):
|
|
343
|
+
# Helper for keyboard inputs that have 1 positive and 1 negative input
|
|
344
|
+
pos_key = self._is_key_pressed.get(key1, False)
|
|
345
|
+
neg_key = self._is_key_pressed.get(key2, False)
|
|
346
|
+
return pos_key - neg_key
|
|
347
|
+
|
|
348
|
+
drive_dir = get_intent_direction(b'w', b's')
|
|
349
|
+
turn_dir = get_intent_direction(b'd', b'a')
|
|
350
|
+
lift_dir = get_intent_direction(b'r', b'f')
|
|
351
|
+
head_dir = get_intent_direction(b't', b'g')
|
|
352
|
+
if drive_dir < 0:
|
|
353
|
+
# It feels more natural to turn the opposite way when reversing
|
|
354
|
+
turn_dir = -turn_dir
|
|
355
|
+
|
|
356
|
+
# Scale drive speeds with SHIFT (faster) and ALT (slower)
|
|
357
|
+
if self._is_shift_down:
|
|
358
|
+
speed_scalar = 2.0
|
|
359
|
+
elif self._is_alt_down:
|
|
360
|
+
speed_scalar = 0.5
|
|
361
|
+
else:
|
|
362
|
+
speed_scalar = 1.0
|
|
363
|
+
|
|
364
|
+
drive_speed = 75.0 * speed_scalar
|
|
365
|
+
turn_speed = 100.0 * speed_scalar
|
|
366
|
+
|
|
367
|
+
left_wheel_speed = (drive_dir * drive_speed) + (turn_speed * turn_dir)
|
|
368
|
+
right_wheel_speed = (drive_dir * drive_speed) - (turn_speed * turn_dir)
|
|
369
|
+
lift_speed = 4.0 * lift_dir * speed_scalar
|
|
370
|
+
head_speed = head_dir * speed_scalar
|
|
371
|
+
|
|
372
|
+
connect_block = self._is_key_pressed.get(b'c', False)
|
|
373
|
+
|
|
374
|
+
control_intents = _RobotControlIntents(left_wheel_speed, right_wheel_speed,
|
|
375
|
+
lift_speed, head_speed, connect_block)
|
|
376
|
+
self._input_intent_queue.put(control_intents, True)
|
|
377
|
+
|
|
378
|
+
def _idle(self):
|
|
379
|
+
if self._is_keyboard_control_enabled:
|
|
380
|
+
self._update_intents_for_robot()
|
|
381
|
+
glutPostRedisplay()
|
|
382
|
+
|
|
383
|
+
def _visible(self, vis):
|
|
384
|
+
# Called from OpenGL when visibility changes (windows are either visible
|
|
385
|
+
# or completely invisible/hidden)
|
|
386
|
+
if vis == GLUT_VISIBLE:
|
|
387
|
+
glutIdleFunc(self._idle)
|
|
388
|
+
else:
|
|
389
|
+
glutIdleFunc(None)
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
#: A default window resolution provided for OpenGL Vector programs
|
|
393
|
+
#: 800x600 is large enough to see detail, while fitting on the smaller
|
|
394
|
+
#: end of modern monitors.
|
|
395
|
+
default_resolution = [800, 600]
|
|
396
|
+
|
|
397
|
+
#: A default projector configurate provided for OpenGL Vector programs
|
|
398
|
+
#: A Field of View of 45 degrees is common for 3d applications,
|
|
399
|
+
#: and a viewable distance range of 1.0 to 1000.0 will provide a
|
|
400
|
+
#: visible space comparable with most physical Vector environments.
|
|
401
|
+
default_projector = opengl.Projector(
|
|
402
|
+
fov=45.0,
|
|
403
|
+
near_clip_plane=1.0,
|
|
404
|
+
far_clip_plane=1000.0)
|
|
405
|
+
|
|
406
|
+
#: A default camera object provided for OpenGL Vector programs.
|
|
407
|
+
#: Starts close to and looking at the charger.
|
|
408
|
+
default_camera = opengl.Camera(
|
|
409
|
+
look_at=util.Vector3(100.0, -25.0, 0.0),
|
|
410
|
+
up=util.Vector3(0.0, 0.0, 1.0),
|
|
411
|
+
distance=500.0,
|
|
412
|
+
pitch=math.radians(40),
|
|
413
|
+
yaw=math.radians(270))
|
|
414
|
+
|
|
415
|
+
#: A default light group provided for OpenGL Vector programs.
|
|
416
|
+
#: Contains one light near the origin.
|
|
417
|
+
default_lights = [opengl.Light(
|
|
418
|
+
ambient_color=[1.0, 1.0, 1.0, 1.0],
|
|
419
|
+
diffuse_color=[1.0, 1.0, 1.0, 1.0],
|
|
420
|
+
specular_color=[1.0, 1.0, 1.0, 1.0],
|
|
421
|
+
position=util.Vector3(0, 32, 20))]
|
|
422
|
+
|
|
423
|
+
# Global viewer instance. Stored to make sure multiple viewers are not
|
|
424
|
+
# instantiated simultaneously.
|
|
425
|
+
opengl_viewer = None # type: OpenGLViewer
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
class OpenGLViewer():
|
|
429
|
+
"""OpenGL-based 3D Viewer.
|
|
430
|
+
|
|
431
|
+
Handles rendering of a 3D world view including navigation map.
|
|
432
|
+
|
|
433
|
+
:param close_event: Used to notify each process when done rendering.
|
|
434
|
+
:type close_event: multiprocessing.Event
|
|
435
|
+
:param input_intent_queue: Sends key commands from the 3D viewer process to the main process.
|
|
436
|
+
:type input_intent_queue: multiprocessing.Queue
|
|
437
|
+
:param nav_map_queue: Updates the 3D viewer process with the latest navigation map.
|
|
438
|
+
:type nav_map_queue: multiprocessing.Queue
|
|
439
|
+
:param world_frame_queue: Provides the 3D viewer with details about the world.
|
|
440
|
+
:type world_frame_queue: multiprocessing.Queue
|
|
441
|
+
:param extra_render_function_queue: Functions to be executed in the 3D viewer process.
|
|
442
|
+
:type extra_render_function_queue: multiprocessing.Queue
|
|
443
|
+
:param user_data_queue: A queue that may be used outside the SDK to pass information to the viewer process.
|
|
444
|
+
May be used by ``extra_render_function_queue`` functions.
|
|
445
|
+
:type user_data_queue: multiprocessing.Queue
|
|
446
|
+
:param resolution: Specifies whether to draw controls on the view.
|
|
447
|
+
:param projector: Specifies whether to draw controls on the view.
|
|
448
|
+
:param camera: Specifies whether to draw controls on the view.
|
|
449
|
+
:param lights: Specifies whether to draw controls on the view.
|
|
450
|
+
:param show_viewer_controls: Specifies whether to draw controls on the view.
|
|
451
|
+
"""
|
|
452
|
+
|
|
453
|
+
def __init__(self,
|
|
454
|
+
close_event: mp.Event,
|
|
455
|
+
input_intent_queue: mp.Queue,
|
|
456
|
+
nav_map_queue: mp.Queue,
|
|
457
|
+
world_frame_queue: mp.Queue,
|
|
458
|
+
extra_render_function_queue: mp.Queue,
|
|
459
|
+
user_data_queue: mp.Queue,
|
|
460
|
+
resolution: List[int] = None,
|
|
461
|
+
projector: opengl.Projector = None,
|
|
462
|
+
camera: opengl.Camera = None,
|
|
463
|
+
lights: List[opengl.Light] = None,
|
|
464
|
+
show_viewer_controls: bool = True):
|
|
465
|
+
if resolution is None:
|
|
466
|
+
resolution = default_resolution
|
|
467
|
+
if projector is None:
|
|
468
|
+
projector = default_projector
|
|
469
|
+
if camera is None:
|
|
470
|
+
camera = default_camera
|
|
471
|
+
if lights is None:
|
|
472
|
+
lights = default_lights
|
|
473
|
+
|
|
474
|
+
self._close_event = close_event
|
|
475
|
+
self._input_intent_queue = input_intent_queue
|
|
476
|
+
self._nav_map_queue = nav_map_queue
|
|
477
|
+
self._world_frame_queue = world_frame_queue
|
|
478
|
+
self._extra_render_function_queue = extra_render_function_queue
|
|
479
|
+
self._user_data_queue = user_data_queue
|
|
480
|
+
|
|
481
|
+
self._logger = util.get_class_logger(__name__, self)
|
|
482
|
+
self._extra_render_calls = []
|
|
483
|
+
|
|
484
|
+
self._internal_function_finished = False
|
|
485
|
+
|
|
486
|
+
# Controls
|
|
487
|
+
self.show_controls = show_viewer_controls
|
|
488
|
+
self._instructions = '\n'.join(['W, S: Move forward, backward',
|
|
489
|
+
'A, D: Turn left, right',
|
|
490
|
+
'R, F: Lift up, down',
|
|
491
|
+
'T, G: Head up, down',
|
|
492
|
+
'',
|
|
493
|
+
'C: Connect to LightCube',
|
|
494
|
+
'',
|
|
495
|
+
'LMB: Rotate camera',
|
|
496
|
+
'RMB: Move camera',
|
|
497
|
+
'LMB + RMB: Move camera up/down',
|
|
498
|
+
'LMB + Z: Zoom camera',
|
|
499
|
+
'X: same as RMB',
|
|
500
|
+
'TAB: center view on robot',
|
|
501
|
+
'',
|
|
502
|
+
'H: Toggle help'])
|
|
503
|
+
|
|
504
|
+
self._vector_view_manifest = opengl_vector.VectorViewManifest()
|
|
505
|
+
self._main_window = opengl.OpenGLWindow(0, 0, resolution[0], resolution[1], b"Vector 3D Visualizer")
|
|
506
|
+
|
|
507
|
+
# Create a 3d projector configuration class.
|
|
508
|
+
self._projector = projector
|
|
509
|
+
self._camera = camera
|
|
510
|
+
self._lights = lights
|
|
511
|
+
|
|
512
|
+
self._view_controller = _OpenGLViewController(self.close, self._camera, self._input_intent_queue, self)
|
|
513
|
+
|
|
514
|
+
self._latest_world_frame: opengl_vector.WorldRenderFrame = None
|
|
515
|
+
|
|
516
|
+
def _render_world_frame(self, world_frame: opengl_vector.WorldRenderFrame):
|
|
517
|
+
"""Render the world to the current OpenGL context
|
|
518
|
+
|
|
519
|
+
:param world_frame: frame to render
|
|
520
|
+
"""
|
|
521
|
+
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL)
|
|
522
|
+
glEnable(GL_NORMALIZE) # to re-scale scaled normals
|
|
523
|
+
|
|
524
|
+
light_cube_view = self._vector_view_manifest.light_cube_view
|
|
525
|
+
unit_cube_view = self._vector_view_manifest.unit_cube_view
|
|
526
|
+
robot_view = self._vector_view_manifest.robot_view
|
|
527
|
+
nav_map_view = self._vector_view_manifest.nav_map_view
|
|
528
|
+
|
|
529
|
+
robot_frame = world_frame.robot_frame
|
|
530
|
+
robot_pose = robot_frame.pose
|
|
531
|
+
|
|
532
|
+
try:
|
|
533
|
+
glDisable(GL_LIGHTING)
|
|
534
|
+
nav_map_view.display()
|
|
535
|
+
|
|
536
|
+
glEnable(GL_LIGHTING)
|
|
537
|
+
# Render the cube
|
|
538
|
+
for obj in world_frame.cube_frames:
|
|
539
|
+
cube_pose = obj.pose
|
|
540
|
+
if cube_pose is not None and cube_pose.is_comparable(robot_pose):
|
|
541
|
+
light_cube_view.display(cube_pose)
|
|
542
|
+
|
|
543
|
+
# Render the custom objects
|
|
544
|
+
for obj in world_frame.custom_object_frames:
|
|
545
|
+
obj_pose = obj.pose
|
|
546
|
+
if obj_pose is not None and obj_pose.is_comparable(robot_pose):
|
|
547
|
+
glPushMatrix()
|
|
548
|
+
obj_matrix = obj_pose.to_matrix()
|
|
549
|
+
glMultMatrixf(obj_matrix.in_row_order)
|
|
550
|
+
|
|
551
|
+
glScalef(obj.x_size_mm * 0.5,
|
|
552
|
+
obj.y_size_mm * 0.5,
|
|
553
|
+
obj.z_size_mm * 0.5)
|
|
554
|
+
|
|
555
|
+
# Only draw solid object for observable custom objects
|
|
556
|
+
|
|
557
|
+
if obj.is_fixed:
|
|
558
|
+
# fixed objects are drawn as transparent outlined boxes to make
|
|
559
|
+
# it clearer that they have no effect on vision.
|
|
560
|
+
FIXED_OBJECT_COLOR = [1.0, 0.7, 0.0, 1.0]
|
|
561
|
+
unit_cube_view.display(FIXED_OBJECT_COLOR, False)
|
|
562
|
+
else:
|
|
563
|
+
CUSTOM_OBJECT_COLOR = [1.0, 0.3, 0.3, 1.0]
|
|
564
|
+
unit_cube_view.display(CUSTOM_OBJECT_COLOR, True)
|
|
565
|
+
|
|
566
|
+
glPopMatrix()
|
|
567
|
+
|
|
568
|
+
glBindTexture(GL_TEXTURE_2D, 0)
|
|
569
|
+
|
|
570
|
+
for face in world_frame.face_frames:
|
|
571
|
+
face_pose = face.pose
|
|
572
|
+
if face_pose is not None and face_pose.is_comparable(robot_pose):
|
|
573
|
+
glPushMatrix()
|
|
574
|
+
face_matrix = face_pose.to_matrix()
|
|
575
|
+
glMultMatrixf(face_matrix.in_row_order)
|
|
576
|
+
|
|
577
|
+
# Approximate size of a head
|
|
578
|
+
glScalef(100, 25, 100)
|
|
579
|
+
|
|
580
|
+
FACE_OBJECT_COLOR = [0.5, 0.5, 0.5, 1.0]
|
|
581
|
+
draw_solid = face.time_since_last_seen < 30
|
|
582
|
+
unit_cube_view.display(FACE_OBJECT_COLOR, draw_solid)
|
|
583
|
+
|
|
584
|
+
glPopMatrix()
|
|
585
|
+
except BaseException as e:
|
|
586
|
+
self._logger.error('rendering error: {0}'.format(e))
|
|
587
|
+
|
|
588
|
+
glDisable(GL_LIGHTING)
|
|
589
|
+
|
|
590
|
+
# Draw the Vector robot to the screen
|
|
591
|
+
robot_view.display(robot_frame.pose, robot_frame.head_angle, robot_frame.lift_position)
|
|
592
|
+
|
|
593
|
+
if self.show_controls:
|
|
594
|
+
self._draw_controls(world_frame.cube_connected(), world_frame.cube_connecting())
|
|
595
|
+
|
|
596
|
+
def _draw_controls(self, cube_connected, cube_connecting):
|
|
597
|
+
try:
|
|
598
|
+
GLUT_BITMAP_9_BY_15
|
|
599
|
+
except NameError:
|
|
600
|
+
pass
|
|
601
|
+
else:
|
|
602
|
+
_draw_text(GLUT_BITMAP_9_BY_15, self._instructions, x=10, y=10)
|
|
603
|
+
if cube_connecting:
|
|
604
|
+
_draw_text(GLUT_BITMAP_9_BY_15, "<connecting...>", x=600, y=10, r=0.75, g=0.5, b=0.0)
|
|
605
|
+
elif cube_connected:
|
|
606
|
+
_draw_text(GLUT_BITMAP_9_BY_15, "<cube connected>", x=600, y=10, r=0.0, g=0.85, b=0.0)
|
|
607
|
+
else:
|
|
608
|
+
_draw_text(GLUT_BITMAP_9_BY_15, "<no cube connected>", x=600, y=10, r=0.75, g=0.75, b=0.75)
|
|
609
|
+
|
|
610
|
+
def _render_3d_view(self, window: opengl.OpenGLWindow):
|
|
611
|
+
"""Renders 3d objects to an openGL window
|
|
612
|
+
|
|
613
|
+
:param window: OpenGL window to render to
|
|
614
|
+
"""
|
|
615
|
+
window.prepare_for_rendering(self._projector, self._camera, self._lights)
|
|
616
|
+
|
|
617
|
+
try:
|
|
618
|
+
extra_render_call = self._extra_render_function_queue.get(False)
|
|
619
|
+
self._extra_render_calls.append(extra_render_call)
|
|
620
|
+
except mp.queues.Empty:
|
|
621
|
+
pass
|
|
622
|
+
|
|
623
|
+
# Update the latest world frame if there is a new one available
|
|
624
|
+
try:
|
|
625
|
+
world_frame = self._world_frame_queue.get(False) # type: WorldRenderFrame
|
|
626
|
+
if world_frame is not None:
|
|
627
|
+
self._view_controller.last_robot_position = world_frame.robot_frame.pose.position
|
|
628
|
+
self._latest_world_frame = world_frame
|
|
629
|
+
except mp.queues.Empty:
|
|
630
|
+
world_frame = self._latest_world_frame
|
|
631
|
+
|
|
632
|
+
try:
|
|
633
|
+
new_nav_map = self._nav_map_queue.get(False)
|
|
634
|
+
if new_nav_map is not None:
|
|
635
|
+
new_nav_map = nav_map.NavMapGrid(new_nav_map, self._logger)
|
|
636
|
+
self._vector_view_manifest.nav_map_view.build_from_nav_map(new_nav_map)
|
|
637
|
+
except mp.queues.Empty:
|
|
638
|
+
# no new nav map - queue is empty
|
|
639
|
+
pass
|
|
640
|
+
|
|
641
|
+
if world_frame is not None:
|
|
642
|
+
self._render_world_frame(world_frame)
|
|
643
|
+
|
|
644
|
+
for render_call in self._extra_render_calls:
|
|
645
|
+
# Protecting the external calls with pushMatrix so internal transform
|
|
646
|
+
# state changes will not alter other calls
|
|
647
|
+
glPushMatrix()
|
|
648
|
+
try:
|
|
649
|
+
render_call.invoke(self._user_data_queue)
|
|
650
|
+
finally:
|
|
651
|
+
glPopMatrix()
|
|
652
|
+
|
|
653
|
+
window.display_rendered_content()
|
|
654
|
+
|
|
655
|
+
def _on_window_update(self):
|
|
656
|
+
"""Top level display call.
|
|
657
|
+
"""
|
|
658
|
+
try:
|
|
659
|
+
self._render_3d_view(self._main_window)
|
|
660
|
+
|
|
661
|
+
except KeyboardInterrupt:
|
|
662
|
+
self._logger.info("_display caught KeyboardInterrupt - exitting")
|
|
663
|
+
self._close_event.set()
|
|
664
|
+
|
|
665
|
+
def run(self):
|
|
666
|
+
"""Turns control of the current thread over to the OpenGL viewer
|
|
667
|
+
"""
|
|
668
|
+
self._main_window.initialize(self._on_window_update)
|
|
669
|
+
self._view_controller.initialize()
|
|
670
|
+
|
|
671
|
+
self._vector_view_manifest.load_assets()
|
|
672
|
+
|
|
673
|
+
# use a non-blocking update loop if possible to make exit conditions
|
|
674
|
+
# easier (not supported on all GLUT versions).
|
|
675
|
+
if bool(glutCheckLoop):
|
|
676
|
+
while not self._close_event.is_set():
|
|
677
|
+
glutCheckLoop()
|
|
678
|
+
else:
|
|
679
|
+
# This blocks until quit
|
|
680
|
+
glutMainLoop()
|
|
681
|
+
|
|
682
|
+
if not self._close_event.is_set():
|
|
683
|
+
# Pass the keyboard interrupt on to SDK so that it can close cleanly
|
|
684
|
+
raise KeyboardInterrupt
|
|
685
|
+
|
|
686
|
+
def close(self):
|
|
687
|
+
"""Called from the SDK when the program is complete and it's time to exit."""
|
|
688
|
+
if not self._close_event.is_set():
|
|
689
|
+
self._close_event.set()
|