-
Notifications
You must be signed in to change notification settings - Fork 0
/
conn.py
486 lines (395 loc) · 20.1 KB
/
conn.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
# Copyright (c) 2016-2017 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.
'''Engine connection.
The SDK operates by connecting to the Cozmo "engine" - typically the Cozmo
app that runs on an iOS or Android device.
The engine is responsible for much of the work that Cozmo does, including
image recognition, path planning, behaviors and animation handling, etc.
The :mod:`cozmo.run` module takes care of opening a connection over a USB
connection to a device, but the :class:`CozmoConnection` class defined in
this module does the work of relaying messages to and from the engine and
dispatching them to the :class:`cozmo.robot.Robot` instance.
'''
# __all__ should order by constants, event classes, other classes, functions.
__all__ = ['EvtRobotFound', 'CozmoConnection']
import asyncio
import platform
import cozmoclad
from . import logger
from . import anim
from . import clad_protocol
from . import event
from . import exceptions
from . import robot
from . import version
from . import _clad
from ._clad import _clad_to_engine_cozmo, _clad_to_engine_iface, _clad_to_game_cozmo, _clad_to_game_iface
class EvtConnected(event.Event):
'''Triggered when the initial connection to the device has been established.
This connection is setup before contacting the robot - Wait for EvtRobotFound
or EvtRobotReady for a usefully configured Cozmo instance.
'''
conn = 'The connected CozmoConnection object'
class EvtRobotFound(event.Event):
'''Triggered when a Cozmo robot is detected, but before he's initialized.
:class:`cozmo.robot.EvtRobotReady` is dispatched when the robot is fully initialized.
'''
robot = 'The Cozmo object for the robot'
class EvtConnectionClosed(event.Event):
'''Triggered when the connection to the controlling device is closed.
'''
exc = 'The exception that triggered the closure, or None'
# Some messages have no robotID but should still be forwarded to the primary robot
FORCED_ROBOT_MESSAGES = {"AnimationAborted",
"AnimationEvent",
"BehaviorObjectiveAchieved",
"BehaviorTransition",
"BlockPickedUp",
"BlockPlaced",
"BlockPoolDataMessage",
"CarryStateUpdate",
"ChargerEvent",
"ConnectedObjectStates",
"CreatedFixedCustomObject",
"CubeLightsStateTransition",
"CurrentCameraParams",
"DefinedCustomObject",
"DeviceAccelerometerValuesRaw",
"DeviceAccelerometerValuesUser",
"DeviceGyroValues",
"IsDeviceIMUSupported",
"LoadedKnownFace",
"LocatedObjectStates",
"MemoryMapMessage",
"MemoryMapMessageBegin",
"MemoryMapMessageEnd",
"ObjectAccel",
"ObjectAvailable",
"ObjectConnectionState",
"ObjectMoved",
"ObjectPowerLevel",
"ObjectProjectsIntoFOV",
"ObjectStoppedMoving",
"ObjectTapped",
"ObjectTappedFiltered",
"ObjectUpAxisChanged",
"PerRobotSettings",
"ReactionaryBehaviorTransition",
"RobotChangedObservedFaceID",
"RobotCliffEventFinished",
"RobotCompletedAction",
"RobotDeletedAllCustomObjects",
"RobotDeletedCustomMarkerObjects",
"RobotDeletedFixedCustomObjects",
"RobotDelocalized",
"RobotErasedAllEnrolledFaces",
"RobotErasedEnrolledFace",
"RobotObservedFace",
"RobotObservedMotion",
"RobotObservedObject",
"RobotObservedPet",
"RobotObservedPossibleObject",
"RobotOnChargerPlatformEvent",
"RobotPoked",
"RobotReachedEnrollmentCount",
"RobotRenamedEnrolledFace",
"RobotState",
"UnexpectedMovement"}
class CozmoConnection(event.Dispatcher, clad_protocol.CLADProtocol):
'''Manages the connection to the Cozmo app to communicate with the core engine.
An instance of this class is passed to functions used with
:func:`cozmo.run.connect`. At the point the function is executed,
the connection is already established and verified, and the
:class:`EvtConnected` has already been sent.
However, after the initial connection is established, programs will usually
want to call :meth:`wait_for_robot` to wait for an actual Cozmo robot to
be detected and initialized before doing useful work.
'''
#: callable: The factory function that returns a
#: :class:`cozmo.robot.Robot` class or subclass instance.
robot_factory = robot.Robot
#: callable: The factory function that returns an
#: :class:`cozmo.anim.AnimationNames` class or subclass instance.
anim_names_factory = anim.AnimationNames
# overrides for CLADProtocol
clad_decode_union = _clad_to_game_iface.MessageEngineToGame
clad_encode_union = _clad_to_engine_iface.MessageGameToEngine
def __init__(self, *a, **kw):
super().__init__(*a, **kw)
self._is_connected = False
self._is_ui_connected = False
self._running = True
self._robots = {}
self._primary_robot = None
#: A dict containing information about the device the connection is using.
self.device_info = {}
#: An :class:`cozmo.anim.AnimationNames` object that references all
#: available animation names
self.anim_names = self.anim_names_factory(self)
#### Private Methods ####
def __repr__(self):
info = ' '.join(['%s="%s"' % (k, self.device_info[k])
for k in sorted(self.device_info.keys())])
return '<%s %s>' % (self.__class__.__name__, info)
def connection_made(self, transport):
super().connection_made(transport)
self._is_connected = True
def connection_lost(self, exc):
super().connection_lost(exc)
self._is_connected = False
if self._running:
self.abort(exceptions.ConnectionAborted("Lost connection to the device"))
logger.error("Lost connection to the device: %s", exc)
async def shutdown(self):
'''Close the connection to the device.'''
if self._running and self._is_connected:
logger.info("Shutting down connection")
self._running = False
event._abort_futures(exceptions.SDKShutdown())
self._stop_dispatcher()
self.transport.close()
def abort(self, exc):
'''Abort the connection to the device.'''
if self._running:
logger.info('Aborting connection: %s', exc)
self._running = False
# Allow any currently pending futures to complete before the
# remainder are aborted.
self._loop.call_soon(lambda: event._abort_futures(exc))
self._stop_dispatcher()
self.transport.close()
def msg_received(self, msg):
'''Receives low level communication messages from the engine.'''
if not self._running:
return
try:
tag_name = msg.tag_name
if tag_name == 'Ping':
# short circuit to avoid unnecessary event overhead
return self._handle_ping(msg._data)
elif tag_name == 'UiDeviceConnected':
# handle outside of event dispatch for quick abort in case
# of a version mismatch problem.
return self._handle_ui_device_connected(msg._data)
msg = msg._data
robot_id = getattr(msg, 'robotID', None)
event_name = '_Msg' + tag_name
evttype = getattr(_clad, event_name, None)
if evttype is None:
logger.error('Received unknown CLAD message %s', event_name)
return
# Dispatch messages to the robot if they either:
# a) are explicitly white listed in FORCED_ROBOT_MESSAGES
# b) have a robotID specified in the message
# Otherwise dispatch the message through this connection.
if (robot_id is not None) or (tag_name in FORCED_ROBOT_MESSAGES):
if robot_id is None:
# The only robot ID ever used is 1, so it is safe to assume that here as a default.
robot_id = 1
self._process_robot_msg(robot_id, evttype, msg)
else:
self.dispatch_event(evttype, msg=msg)
except Exception as exc:
# No exceptions should reach this point; it's a bug if they do.
self.abort(exc)
def _process_robot_msg(self, robot_id, evttype, msg):
if robot_id != 1:
# Note: some messages replace robotID with value!=1 (like mfgID for example)
# as a result, this log may fire quite often. Log Level is set to debug
# since it suppressed by default (prevents spamming).
logger.debug('INVALID ROBOT_ID SEEN robot_id=%s event=%s msg=%s', robot_id, evttype, msg.__str__())
robot_id = 1 # XXX remove when errant messages have been fixed
# Note: this code constructs the robot if it doesn't exist at this time
robot = self._robots.get(robot_id)
if not robot:
logger.info('Found robot id=%s', robot_id)
robot = self.robot_factory(self, robot_id, is_primary=self._primary_robot is None)
self._robots[robot_id] = robot
if not self._primary_robot:
self._primary_robot = robot
# Dispatch an event notifying that a new robot has been found
# the robot itself will send EvtRobotReady after initialization
self.dispatch_event(EvtRobotFound, robot=robot)
# _initialize will set the robot to a known good state in the
# background and dispatch a EvtRobotReady event when completed.
robot._initialize()
robot.dispatch_event(evttype, msg=msg)
#### Properties ####
@property
def is_connected(self):
'''bool: True if currently connected to the remote engine.'''
return self._is_connected
#### Private Event handlers ####
def _handle_ping(self, msg):
'''Respond to a ping event.'''
if msg.isResponse:
# To avoid duplication, pings originate from engine, and engine
# accumulates the latency info from the responses
logger.error("Only engine should receive responses")
else:
resp = _clad_to_engine_iface.Ping(
counter=msg.counter,
timeSent_ms=msg.timeSent_ms,
isResponse=True)
self.send_msg(resp)
def _recv_default_handler(self, event, **kw):
'''Default event handler.'''
if event.event_name.startswith('msg_animation'):
return self.anim.dispatch_event(event)
logger.debug('Engine received unhandled event_name=%s kw=%s', event, kw)
def _recv_msg_animation_available(self, evt, msg):
self.anim_names.dispatch_event(evt)
def _recv_msg_end_of_message(self, evt, *a, **kw):
self.anim_names.dispatch_event(evt)
def _handle_ui_device_connected(self, msg):
if msg.connectionType != _clad_to_engine_cozmo.UiConnectionType.SdkOverTcp:
# This isn't for us
return
if msg.deviceID != 1:
logger.error('Unexpected Device Id %s', msg.deviceID)
return
# Verify that engine and SDK are compatible
clad_hashes_match = False
try:
cozmoclad.assert_clad_match(msg.toGameCLADHash, msg.toEngineCLADHash)
clad_hashes_match = True
except cozmoclad.CLADHashMismatch as exc:
logger.error(exc)
build_versions_match = (cozmoclad.__build_version__ == '00000.00000.00000'
or cozmoclad.__build_version__ == msg.buildVersion)
if clad_hashes_match and not build_versions_match:
# If CLAD hashes match, and this is only a minor version change,
# then still allow connection (it's just an app hotfix
# that didn't require CLAD or SDK changes)
sdk_major_version = cozmoclad.__build_version__.split(".")[0:2]
build_major_version = msg.buildVersion.split(".")[0:2]
build_versions_match = (sdk_major_version == build_major_version)
if clad_hashes_match and build_versions_match:
connection_success_msg = _clad_to_engine_iface.UiDeviceConnectionSuccess(
connectionType=msg.connectionType,
deviceID=msg.deviceID,
buildVersion = cozmoclad.__version__,
sdkModuleVersion = version.__version__,
pythonVersion = platform.python_version(),
pythonImplementation = platform.python_implementation(),
osVersion = platform.platform(),
cpuVersion = platform.machine())
self.send_msg(connection_success_msg)
else:
try:
wrong_version_msg = _clad_to_engine_iface.UiDeviceConnectionWrongVersion(
reserved=0,
connectionType=msg.connectionType,
deviceID = msg.deviceID,
buildVersion = cozmoclad.__version__)
self.send_msg(wrong_version_msg)
except AttributeError:
pass
line_separator = "=" * 80
error_message = "\n" + line_separator + "\n"
def _trimmed_version(ver_string):
# Trim leading zeros from the version string.
trimmed_string = ""
for i in ver_string.split("."):
trimmed_string += str(int(i)) + "."
return trimmed_string[:-1] # remove trailing "."
if not build_versions_match:
error_message += ("App and SDK versions do not match!\n"
"----------------------------------\n"
"SDK's cozmoclad version: %s\n"
" != app version: %s\n\n"
% (cozmoclad.__version__, _trimmed_version(msg.buildVersion)))
if cozmoclad.__build_version__ < msg.buildVersion:
# App is newer
error_message += ('Please update your SDK to the newest version by calling command:\n'
'"pip3 install --user --upgrade cozmo"\n'
'and downloading the latest examples from:\n'
'http://cozmosdk.anki.com/docs/downloads.html\n')
else:
# SDK is newer
error_message += ('Please either:\n\n'
'1) Update your app to the most recent version on the app store.\n'
'2) Or, if you prefer, please determine which SDK version matches\n'
' your app version at: http://go.anki.com/cozmo-sdk-version\n'
' Then downgrade your SDK by calling the following command,\n'
' replacing SDK_VERSION with the version listed at that page:\n'
' "pip3 install --ignore-installed cozmo==SDK_VERSION"\n')
else:
# CLAD version mismatch
error_message += ('CLAD Hashes do not match!\n'
'-------------------------\n'
'Your Python and C++ CLAD versions do not match - connection refused.\n'
'Please check that you have the most recent versions of both the SDK and the\n'
'Cozmo app. You may update your SDK by calling:\n'
'"pip3 install --user --upgrade cozmo".\n'
'Please also check the app store for a Cozmo app update.\n')
error_message += line_separator
logger.error(error_message)
exc = exceptions.SDKVersionMismatch("SDK library does not match software running on device",
sdk_version=version.__version__,
sdk_app_version=cozmoclad.__version__,
app_version=_trimmed_version(msg.buildVersion))
self._abort_connection = True # Ignore remaining messages - they're not safe to unpack
self.abort(exc)
return
self._is_ui_connected = True
self.dispatch_event(EvtConnected, conn=self)
logger.info('App connection established. sdk_version=%s '
'cozmoclad_version=%s app_build_version=%s',
version.__version__, cozmoclad.__version__, msg.buildVersion)
# We send RequestConnectedObjects and RequestLocatedObjectStates before
# refreshing the animation names as this ensures that we will receive
# the responses before we mark the robot as ready.
self._request_connected_objects()
self._request_located_objects()
self.anim_names.refresh()
def _request_connected_objects(self):
# Request information on connected objects (e.g. the object ID of each cube)
# (this won't provide location/pose info)
msg = _clad_to_engine_iface.RequestConnectedObjects()
self.send_msg(msg)
def _request_located_objects(self):
# Request the pose information for all objects whose location we know
# (this won't include any objects where the location is currently not known)
msg = _clad_to_engine_iface.RequestLocatedObjectStates()
self.send_msg(msg)
def _recv_msg_image_chunk(self, evt, *, msg):
if self._primary_robot:
self._primary_robot.dispatch_event(evt)
#### Public Event Handlers ####
#### Commands ####
async def _wait_for_robot(self, timeout=5):
if not self._primary_robot:
await self.wait_for(EvtRobotFound, timeout=timeout)
if self._primary_robot.is_ready:
return self._primary_robot
await self._primary_robot.wait_for(robot.EvtRobotReady, timeout=timeout)
return self._primary_robot
async def wait_for_robot(self, timeout=5):
'''Wait for a Cozmo robot to connect and complete initialization.
Args:
timeout (float): Maximum length of time to wait for a robot to be ready in seconds.
Returns:
A :class:`cozmo.robot.Robot` instance that's ready to use.
Raises:
:class:`asyncio.TimeoutError` if there's no response from the robot.
'''
try:
robot = await self._wait_for_robot(timeout)
if robot and robot.drive_off_charger_on_connect:
await robot.drive_off_charger_contacts().wait_for_completed()
except asyncio.TimeoutError:
logger.error('Timed out waiting for robot to initialize')
raise
return robot