@@ -0,0 +1,13 @@ | |||
<?xml version="1.0" encoding="UTF-8"?> | |||
<module type="PYTHON_MODULE" version="4"> | |||
<component name="NewModuleRootManager"> | |||
<content url="file://$MODULE_DIR$"> | |||
<excludeFolder url="file://$MODULE_DIR$/venv" /> | |||
</content> | |||
<orderEntry type="inheritedJdk" /> | |||
<orderEntry type="sourceFolder" forTests="false" /> | |||
</component> | |||
<component name="TestRunnerService"> | |||
<option name="PROJECT_TEST_RUNNER" value="Unittests" /> | |||
</component> | |||
</module> |
@@ -0,0 +1,7 @@ | |||
<?xml version="1.0" encoding="UTF-8"?> | |||
<project version="4"> | |||
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.6 (Cozmo_LineFollow)" project-jdk-type="Python SDK" /> | |||
<component name="PyCharmProfessionalAdvertiser"> | |||
<option name="shown" value="true" /> | |||
</component> | |||
</project> |
@@ -0,0 +1,8 @@ | |||
<?xml version="1.0" encoding="UTF-8"?> | |||
<project version="4"> | |||
<component name="ProjectModuleManager"> | |||
<modules> | |||
<module fileurl="file://$PROJECT_DIR$/.idea/Cozmo_LineFollow.iml" filepath="$PROJECT_DIR$/.idea/Cozmo_LineFollow.iml" /> | |||
</modules> | |||
</component> | |||
</project> |
@@ -0,0 +1,252 @@ | |||
<?xml version="1.0" encoding="UTF-8"?> | |||
<project version="4"> | |||
<component name="ChangeListManager"> | |||
<list default="true" id="6890b9c1-6701-47f4-b269-98dc98eee6bc" name="Default Changelist" comment="" /> | |||
<option name="EXCLUDED_CONVERTED_TO_IGNORED" value="true" /> | |||
<option name="SHOW_DIALOG" value="false" /> | |||
<option name="HIGHLIGHT_CONFLICTS" value="true" /> | |||
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" /> | |||
<option name="LAST_RESOLUTION" value="IGNORE" /> | |||
</component> | |||
<component name="FileEditorManager"> | |||
<leaf SIDE_TABS_SIZE_LIMIT_KEY="300"> | |||
<file pinned="false" current-in-tab="true"> | |||
<entry file="file://$PROJECT_DIR$/venv/Scripts/CozmoDrive.py"> | |||
<provider selected="true" editor-type-id="text-editor"> | |||
<state relative-caret-position="207"> | |||
<caret line="16" column="60" lean-forward="true" selection-start-line="16" selection-start-column="60" selection-end-line="16" selection-end-column="60" /> | |||
<folding> | |||
<element signature="e#0#12#0" expanded="true" /> | |||
</folding> | |||
</state> | |||
</provider> | |||
</entry> | |||
</file> | |||
<file pinned="false" current-in-tab="false"> | |||
<entry file="file://$USER_HOME$/Downloads/Cozmo-Explorer-Tool-master/remote_control.py"> | |||
<provider selected="true" editor-type-id="text-editor"> | |||
<state relative-caret-position="4182"> | |||
<caret line="276" selection-start-line="276" selection-end-line="276" /> | |||
</state> | |||
</provider> | |||
</entry> | |||
</file> | |||
</leaf> | |||
</component> | |||
<component name="FileTemplateManagerImpl"> | |||
<option name="RECENT_TEMPLATES"> | |||
<list> | |||
<option value="Python Script" /> | |||
</list> | |||
</option> | |||
</component> | |||
<component name="IdeDocumentHistory"> | |||
<option name="CHANGED_PATHS"> | |||
<list> | |||
<option value="$PROJECT_DIR$/venv/Scripts/CozmoDrive.py" /> | |||
</list> | |||
</option> | |||
</component> | |||
<component name="ProjectFrameBounds" extendedState="6"> | |||
<option name="x" value="948" /> | |||
<option name="width" value="981" /> | |||
<option name="height" value="1032" /> | |||
</component> | |||
<component name="ProjectView"> | |||
<navigator proportions="" version="1"> | |||
<foldersAlwaysOnTop value="true" /> | |||
</navigator> | |||
<panes> | |||
<pane id="Scope" /> | |||
<pane id="ProjectPane"> | |||
<subPane> | |||
<expand> | |||
<path> | |||
<item name="Cozmo_LineFollow" type="b2602c69:ProjectViewProjectNode" /> | |||
<item name="Cozmo_LineFollow" type="462c0819:PsiDirectoryNode" /> | |||
</path> | |||
<path> | |||
<item name="Cozmo_LineFollow" type="b2602c69:ProjectViewProjectNode" /> | |||
<item name="Cozmo_LineFollow" type="462c0819:PsiDirectoryNode" /> | |||
<item name="venv" type="462c0819:PsiDirectoryNode" /> | |||
</path> | |||
<path> | |||
<item name="Cozmo_LineFollow" type="b2602c69:ProjectViewProjectNode" /> | |||
<item name="Cozmo_LineFollow" type="462c0819:PsiDirectoryNode" /> | |||
<item name="venv" type="462c0819:PsiDirectoryNode" /> | |||
<item name="Scripts" type="462c0819:PsiDirectoryNode" /> | |||
</path> | |||
<path> | |||
<item name="Cozmo_LineFollow" type="b2602c69:ProjectViewProjectNode" /> | |||
<item name="External Libraries" type="cb654da1:ExternalLibrariesNode" /> | |||
</path> | |||
<path> | |||
<item name="Cozmo_LineFollow" type="b2602c69:ProjectViewProjectNode" /> | |||
<item name="External Libraries" type="cb654da1:ExternalLibrariesNode" /> | |||
<item name="< Python 3.6 (Cozmo_LineFollow) >" type="70bed36:NamedLibraryElementNode" /> | |||
</path> | |||
<path> | |||
<item name="Cozmo_LineFollow" type="b2602c69:ProjectViewProjectNode" /> | |||
<item name="External Libraries" type="cb654da1:ExternalLibrariesNode" /> | |||
<item name="< Python 3.6 (Cozmo_LineFollow) >" type="70bed36:NamedLibraryElementNode" /> | |||
<item name="venv" type="462c0819:PsiDirectoryNode" /> | |||
</path> | |||
<path> | |||
<item name="Cozmo_LineFollow" type="b2602c69:ProjectViewProjectNode" /> | |||
<item name="External Libraries" type="cb654da1:ExternalLibrariesNode" /> | |||
<item name="< Python 3.6 (Cozmo_LineFollow) >" type="70bed36:NamedLibraryElementNode" /> | |||
<item name="venv" type="462c0819:PsiDirectoryNode" /> | |||
<item name="Scripts" type="462c0819:PsiDirectoryNode" /> | |||
</path> | |||
</expand> | |||
<select /> | |||
</subPane> | |||
</pane> | |||
</panes> | |||
</component> | |||
<component name="PropertiesComponent"> | |||
<property name="last_opened_file_path" value="$USER_HOME$/Desktop/cozmoTest.py" /> | |||
<property name="settings.editor.selected.configurable" value="com.jetbrains.python.configuration.PyActiveSdkModuleConfigurable" /> | |||
</component> | |||
<component name="RunDashboard"> | |||
<option name="ruleStates"> | |||
<list> | |||
<RuleState> | |||
<option name="name" value="ConfigurationTypeDashboardGroupingRule" /> | |||
</RuleState> | |||
<RuleState> | |||
<option name="name" value="StatusDashboardGroupingRule" /> | |||
</RuleState> | |||
</list> | |||
</option> | |||
</component> | |||
<component name="RunManager" selected="Python.CozmoDrive"> | |||
<configuration name="CozmoDrive" type="PythonConfigurationType" factoryName="Python" temporary="true"> | |||
<module name="Cozmo_LineFollow" /> | |||
<option name="INTERPRETER_OPTIONS" value="" /> | |||
<option name="PARENT_ENVS" value="true" /> | |||
<envs> | |||
<env name="PYTHONUNBUFFERED" value="1" /> | |||
</envs> | |||
<option name="SDK_HOME" value="" /> | |||
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/venv/Scripts" /> | |||
<option name="IS_MODULE_SDK" value="true" /> | |||
<option name="ADD_CONTENT_ROOTS" value="true" /> | |||
<option name="ADD_SOURCE_ROOTS" value="true" /> | |||
<option name="SCRIPT_NAME" value="$PROJECT_DIR$/venv/Scripts/CozmoDrive.py" /> | |||
<option name="PARAMETERS" value="" /> | |||
<option name="SHOW_COMMAND_LINE" value="false" /> | |||
<option name="EMULATE_TERMINAL" value="false" /> | |||
<option name="MODULE_MODE" value="false" /> | |||
<option name="REDIRECT_INPUT" value="false" /> | |||
<option name="INPUT_FILE" value="" /> | |||
<method v="2" /> | |||
</configuration> | |||
<configuration name="canny_test" type="PythonConfigurationType" factoryName="Python" temporary="true"> | |||
<module name="Cozmo_LineFollow" /> | |||
<option name="INTERPRETER_OPTIONS" value="" /> | |||
<option name="PARENT_ENVS" value="true" /> | |||
<envs> | |||
<env name="PYTHONUNBUFFERED" value="1" /> | |||
</envs> | |||
<option name="SDK_HOME" value="" /> | |||
<option name="WORKING_DIRECTORY" value="$USER_HOME$/Desktop" /> | |||
<option name="IS_MODULE_SDK" value="false" /> | |||
<option name="ADD_CONTENT_ROOTS" value="true" /> | |||
<option name="ADD_SOURCE_ROOTS" value="true" /> | |||
<option name="SCRIPT_NAME" value="$USER_HOME$/Desktop/canny_test.py" /> | |||
<option name="PARAMETERS" value="" /> | |||
<option name="SHOW_COMMAND_LINE" value="false" /> | |||
<option name="EMULATE_TERMINAL" value="false" /> | |||
<option name="MODULE_MODE" value="false" /> | |||
<option name="REDIRECT_INPUT" value="false" /> | |||
<option name="INPUT_FILE" value="" /> | |||
<method v="2" /> | |||
</configuration> | |||
<recent_temporary> | |||
<list> | |||
<item itemvalue="Python.CozmoDrive" /> | |||
<item itemvalue="Python.canny_test" /> | |||
</list> | |||
</recent_temporary> | |||
</component> | |||
<component name="SvnConfiguration"> | |||
<configuration /> | |||
</component> | |||
<component name="TaskManager"> | |||
<task active="true" id="Default" summary="Default task"> | |||
<changelist id="6890b9c1-6701-47f4-b269-98dc98eee6bc" name="Default Changelist" comment="" /> | |||
<created>1556208118986</created> | |||
<option name="number" value="Default" /> | |||
<option name="presentableId" value="Default" /> | |||
<updated>1556208118986</updated> | |||
</task> | |||
<servers /> | |||
</component> | |||
<component name="ToolWindowManager"> | |||
<frame x="-7" y="-7" width="1295" height="695" extended-state="6" /> | |||
<layout> | |||
<window_info content_ui="combo" id="Project" order="0" visible="true" weight="0.1223193" /> | |||
<window_info id="Structure" order="1" side_tool="true" weight="0.25" /> | |||
<window_info id="Favorites" order="2" side_tool="true" /> | |||
<window_info anchor="bottom" id="Message" order="0" /> | |||
<window_info anchor="bottom" id="Find" order="1" weight="0.32978722" /> | |||
<window_info anchor="bottom" id="Run" order="2" weight="0.18439716" /> | |||
<window_info anchor="bottom" id="Debug" order="3" weight="0.4" /> | |||
<window_info anchor="bottom" id="Cvs" order="4" weight="0.25" /> | |||
<window_info anchor="bottom" id="Inspection" order="5" weight="0.4" /> | |||
<window_info anchor="bottom" id="TODO" order="6" /> | |||
<window_info anchor="bottom" id="Version Control" order="7" /> | |||
<window_info anchor="bottom" id="Terminal" order="8" /> | |||
<window_info anchor="bottom" id="Event Log" order="9" side_tool="true" /> | |||
<window_info anchor="bottom" id="Python Console" order="10" /> | |||
<window_info anchor="right" id="Commander" internal_type="SLIDING" order="0" type="SLIDING" weight="0.4" /> | |||
<window_info anchor="right" id="Ant Build" order="1" weight="0.25" /> | |||
<window_info anchor="right" content_ui="combo" id="Hierarchy" order="2" weight="0.25" /> | |||
</layout> | |||
</component> | |||
<component name="editorHistoryManager"> | |||
<entry file="file://$USER_HOME$/Desktop/cozmoTest.py"> | |||
<provider selected="true" editor-type-id="text-editor"> | |||
<state> | |||
<caret selection-end-line="17" selection-end-column="32" /> | |||
<folding> | |||
<element signature="e#0#12#0" expanded="true" /> | |||
</folding> | |||
</state> | |||
</provider> | |||
</entry> | |||
<entry file="file://$USER_HOME$/Downloads/Cozmo-Explorer-Tool-master/explorer_tool.py"> | |||
<provider selected="true" editor-type-id="text-editor"> | |||
<state relative-caret-position="-620" /> | |||
</provider> | |||
</entry> | |||
<entry file="file://$USER_HOME$/Downloads/Cozmo-Explorer-Tool-master/remote_control.py"> | |||
<provider selected="true" editor-type-id="text-editor"> | |||
<state relative-caret-position="4182"> | |||
<caret line="276" selection-start-line="276" selection-end-line="276" /> | |||
</state> | |||
</provider> | |||
</entry> | |||
<entry file="file://$USER_HOME$/Desktop/canny_test.py"> | |||
<provider selected="true" editor-type-id="text-editor"> | |||
<state relative-caret-position="68"> | |||
<caret line="4" column="30" selection-start-line="4" selection-start-column="30" selection-end-line="4" selection-end-column="30" /> | |||
<folding> | |||
<element signature="e#0#10#0" expanded="true" /> | |||
</folding> | |||
</state> | |||
</provider> | |||
</entry> | |||
<entry file="file://$PROJECT_DIR$/venv/Scripts/CozmoDrive.py"> | |||
<provider selected="true" editor-type-id="text-editor"> | |||
<state relative-caret-position="207"> | |||
<caret line="16" column="60" lean-forward="true" selection-start-line="16" selection-start-column="60" selection-end-line="16" selection-end-column="60" /> | |||
<folding> | |||
<element signature="e#0#12#0" expanded="true" /> | |||
</folding> | |||
</state> | |||
</provider> | |||
</entry> | |||
</component> | |||
</project> |
@@ -0,0 +1 @@ | |||
pip |
@@ -0,0 +1,180 @@ | |||
Unless otherwise stated in that file, or the folder containing that file, all | |||
files in the Cozmo SDK are Copyright (c) 2016-2017 Anki Inc. and licensed under | |||
the Apache 2.0 License: | |||
Apache License | |||
Version 2.0, January 2004 | |||
http://www.apache.org/licenses/ | |||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION | |||
1. Definitions. | |||
"License" shall mean the terms and conditions for use, reproduction, | |||
and distribution as defined by Sections 1 through 9 of this document. | |||
"Licensor" shall mean the copyright owner or entity authorized by | |||
the copyright owner that is granting the License. | |||
"Legal Entity" shall mean the union of the acting entity and all | |||
other entities that control, are controlled by, or are under common | |||
control with that entity. For the purposes of this definition, | |||
"control" means (i) the power, direct or indirect, to cause the | |||
direction or management of such entity, whether by contract or | |||
otherwise, or (ii) ownership of fifty percent (50%) or more of the | |||
outstanding shares, or (iii) beneficial ownership of such entity. | |||
"You" (or "Your") shall mean an individual or Legal Entity | |||
exercising permissions granted by this License. | |||
"Source" form shall mean the preferred form for making modifications, | |||
including but not limited to software source code, documentation | |||
source, and configuration files. | |||
"Object" form shall mean any form resulting from mechanical | |||
transformation or translation of a Source form, including but | |||
not limited to compiled object code, generated documentation, | |||
and conversions to other media types. | |||
"Work" shall mean the work of authorship, whether in Source or | |||
Object form, made available under the License, as indicated by a | |||
copyright notice that is included in or attached to the work | |||
(an example is provided in the Appendix below). | |||
"Derivative Works" shall mean any work, whether in Source or Object | |||
form, that is based on (or derived from) the Work and for which the | |||
editorial revisions, annotations, elaborations, or other modifications | |||
represent, as a whole, an original work of authorship. For the purposes | |||
of this License, Derivative Works shall not include works that remain | |||
separable from, or merely link (or bind by name) to the interfaces of, | |||
the Work and Derivative Works thereof. | |||
"Contribution" shall mean any work of authorship, including | |||
the original version of the Work and any modifications or additions | |||
to that Work or Derivative Works thereof, that is intentionally | |||
submitted to Licensor for inclusion in the Work by the copyright owner | |||
or by an individual or Legal Entity authorized to submit on behalf of | |||
the copyright owner. For the purposes of this definition, "submitted" | |||
means any form of electronic, verbal, or written communication sent | |||
to the Licensor or its representatives, including but not limited to | |||
communication on electronic mailing lists, source code control systems, | |||
and issue tracking systems that are managed by, or on behalf of, the | |||
Licensor for the purpose of discussing and improving the Work, but | |||
excluding communication that is conspicuously marked or otherwise | |||
designated in writing by the copyright owner as "Not a Contribution." | |||
"Contributor" shall mean Licensor and any individual or Legal Entity | |||
on behalf of whom a Contribution has been received by Licensor and | |||
subsequently incorporated within the Work. | |||
2. Grant of Copyright License. Subject to the terms and conditions of | |||
this License, each Contributor hereby grants to You a perpetual, | |||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable | |||
copyright license to reproduce, prepare Derivative Works of, | |||
publicly display, publicly perform, sublicense, and distribute the | |||
Work and such Derivative Works in Source or Object form. | |||
3. Grant of Patent License. Subject to the terms and conditions of | |||
this License, each Contributor hereby grants to You a perpetual, | |||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable | |||
(except as stated in this section) patent license to make, have made, | |||
use, offer to sell, sell, import, and otherwise transfer the Work, | |||
where such license applies only to those patent claims licensable | |||
by such Contributor that are necessarily infringed by their | |||
Contribution(s) alone or by combination of their Contribution(s) | |||
with the Work to which such Contribution(s) was submitted. If You | |||
institute patent litigation against any entity (including a | |||
cross-claim or counterclaim in a lawsuit) alleging that the Work | |||
or a Contribution incorporated within the Work constitutes direct | |||
or contributory patent infringement, then any patent licenses | |||
granted to You under this License for that Work shall terminate | |||
as of the date such litigation is filed. | |||
4. Redistribution. You may reproduce and distribute copies of the | |||
Work or Derivative Works thereof in any medium, with or without | |||
modifications, and in Source or Object form, provided that You | |||
meet the following conditions: | |||
(a) You must give any other recipients of the Work or | |||
Derivative Works a copy of this License; and | |||
(b) You must cause any modified files to carry prominent notices | |||
stating that You changed the files; and | |||
(c) You must retain, in the Source form of any Derivative Works | |||
that You distribute, all copyright, patent, trademark, and | |||
attribution notices from the Source form of the Work, | |||
excluding those notices that do not pertain to any part of | |||
the Derivative Works; and | |||
(d) If the Work includes a "NOTICE" text file as part of its | |||
distribution, then any Derivative Works that You distribute must | |||
include a readable copy of the attribution notices contained | |||
within such NOTICE file, excluding those notices that do not | |||
pertain to any part of the Derivative Works, in at least one | |||
of the following places: within a NOTICE text file distributed | |||
as part of the Derivative Works; within the Source form or | |||
documentation, if provided along with the Derivative Works; or, | |||
within a display generated by the Derivative Works, if and | |||
wherever such third-party notices normally appear. The contents | |||
of the NOTICE file are for informational purposes only and | |||
do not modify the License. You may add Your own attribution | |||
notices within Derivative Works that You distribute, alongside | |||
or as an addendum to the NOTICE text from the Work, provided | |||
that such additional attribution notices cannot be construed | |||
as modifying the License. | |||
You may add Your own copyright statement to Your modifications and | |||
may provide additional or different license terms and conditions | |||
for use, reproduction, or distribution of Your modifications, or | |||
for any such Derivative Works as a whole, provided Your use, | |||
reproduction, and distribution of the Work otherwise complies with | |||
the conditions stated in this License. | |||
5. Submission of Contributions. Unless You explicitly state otherwise, | |||
any Contribution intentionally submitted for inclusion in the Work | |||
by You to the Licensor shall be under the terms and conditions of | |||
this License, without any additional terms or conditions. | |||
Notwithstanding the above, nothing herein shall supersede or modify | |||
the terms of any separate license agreement you may have executed | |||
with Licensor regarding such Contributions. | |||
6. Trademarks. This License does not grant permission to use the trade | |||
names, trademarks, service marks, or product names of the Licensor, | |||
except as required for reasonable and customary use in describing the | |||
origin of the Work and reproducing the content of the NOTICE file. | |||
7. Disclaimer of Warranty. Unless required by applicable law or | |||
agreed to in writing, Licensor provides the Work (and each | |||
Contributor provides its Contributions) on an "AS IS" BASIS, | |||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | |||
implied, including, without limitation, any warranties or conditions | |||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A | |||
PARTICULAR PURPOSE. You are solely responsible for determining the | |||
appropriateness of using or redistributing the Work and assume any | |||
risks associated with Your exercise of permissions under this License. | |||
8. Limitation of Liability. In no event and under no legal theory, | |||
whether in tort (including negligence), contract, or otherwise, | |||
unless required by applicable law (such as deliberate and grossly | |||
negligent acts) or agreed to in writing, shall any Contributor be | |||
liable to You for damages, including any direct, indirect, special, | |||
incidental, or consequential damages of any character arising as a | |||
result of this License or out of the use or inability to use the | |||
Work (including but not limited to damages for loss of goodwill, | |||
work stoppage, computer failure or malfunction, or any and all | |||
other commercial damages or losses), even if such Contributor | |||
has been advised of the possibility of such damages. | |||
9. Accepting Warranty or Additional Liability. While redistributing | |||
the Work or Derivative Works thereof, You may choose to offer, | |||
and charge a fee for, acceptance of support, warranty, indemnity, | |||
or other liability obligations and/or rights consistent with this | |||
License. However, in accepting such obligations, You may act only | |||
on Your own behalf and on Your sole responsibility, not on behalf | |||
of any other Contributor, and only if You agree to indemnify, | |||
defend, and hold each Contributor harmless for any liability | |||
incurred by, or claims asserted against, such Contributor by reason | |||
of your accepting any such warranty or additional liability. | |||
END OF TERMS AND CONDITIONS |
@@ -0,0 +1,42 @@ | |||
Metadata-Version: 2.1 | |||
Name: cozmo | |||
Version: 1.4.10 | |||
Summary: SDK for Anki Cozmo, the small robot with the big personality | |||
Home-page: https://developer.anki.com | |||
Author: Anki, Inc | |||
Author-email: developer@anki.com | |||
License: Apache License, Version 2.0 | |||
Keywords: anki,cozmo,robot,robotics,sdk | |||
Platform: UNKNOWN | |||
Classifier: Development Status :: 5 - Production/Stable | |||
Classifier: Intended Audience :: Developers | |||
Classifier: Topic :: Software Development :: Libraries | |||
Classifier: License :: OSI Approved :: Apache Software License | |||
Classifier: Programming Language :: Python :: 3.5 | |||
Requires-Dist: cozmoclad (==3.4.0) | |||
Provides-Extra: 3dviewer | |||
Requires-Dist: PyOpenGL (>=3.1) ; extra == '3dviewer' | |||
Requires-Dist: Pillow (>=3.3) ; extra == '3dviewer' | |||
Requires-Dist: numpy (>=1.11) ; extra == '3dviewer' | |||
Provides-Extra: camera | |||
Requires-Dist: Pillow (>=3.3) ; extra == 'camera' | |||
Requires-Dist: numpy (>=1.11) ; extra == 'camera' | |||
Provides-Extra: test | |||
Requires-Dist: tox ; extra == 'test' | |||
Requires-Dist: pytest ; extra == 'test' | |||
The Cozmo SDK is a flexible vision-based robotics platform used in enterprise, education, and entertainment. | |||
Cozmo’s pioneering combination of advanced robotics hardware and software are part of what make him an innovative consumer experience. But it’s also what makes him, in conjunction with the Cozmo SDK, a groundbreaking robotics platform that’s expressive, engaging, and entertaining. | |||
We built the Cozmo SDK to be robust enough for enterprise and research, but simple enough for anyone with a bit of technical know-how to tap into our sophisticated robotics and AI technologies. Organizations and institutions using the Cozmo SDK include SAP, Oracle, Carnegie Mellon University, and Georgia Tech. Find out more at developer.anki.com | |||
Cozmo SDK documentation: http://cozmosdk.anki.com/docs/ | |||
Official developer forum: https://forums.anki.com/ | |||
Requirements: | |||
* Python 3.5.1 or later | |||
@@ -0,0 +1,78 @@ | |||
cozmo/LICENSE.txt,sha256=THPe12iw4yd8mbwjmhwsGiCsmC2QDopJnGNSbSh9hkc,10356 | |||
cozmo/__init__.py,sha256=VqMKC1E1d4zJQh5P4ezQ9mdqbat8Tq25yOsGN0SnuxE,3118 | |||
cozmo/_clad.py,sha256=jZ_8SzcTtXiZGmd7QNgJMBZPCoYp8ryUGs_FixVaLSA,6643 | |||
cozmo/action.py,sha256=nabVR7VhR9FJPukMGDzHANsPwWvSDQUleKimiYouQx0,27723 | |||
cozmo/anim.py,sha256=W3YbvFW5yz7hw2MGR9_1rOzVKnTNTWUJ_P6nMBAvI4M,8451 | |||
cozmo/annotate.py,sha256=7Ewbp5iF8BwnY83tSP9cd79cm20aezz5WjxJzKOuwz0,20962 | |||
cozmo/audio.py,sha256=2XcawlEve8rujB07Rm_INTPg-5UbwynlU1vD-h_pQ5w,26063 | |||
cozmo/base.py,sha256=EX_oNAph_jXkz_PnLUvxmfXWvHlQbwZRsXSM8rW40MY,8752 | |||
cozmo/behavior.py,sha256=1vSpuKrsnKSwTJDI_jxFiJfI3WBrvT1MZc2XlMQQfm0,7610 | |||
cozmo/camera.py,sha256=qbh8o1EA9DO9dgcvQ-Gigxkikmppa_6NeTNaAh8pctc,24340 | |||
cozmo/clad_protocol.py,sha256=IPVM2Fq1kVV7IVg2-TIhKBD6eyQO7ggy6a7SzeM2rdE,3663 | |||
cozmo/conn.py,sha256=PtIp70ChvgHXKLm0T6Z65WvMgan3oeR1SUErixL0b3o,20614 | |||
cozmo/event.py,sha256=i8yQ3rdQLE1eYg8Pfmmj-W6JKtwK_Zyyy6CnT7haUB8,23195 | |||
cozmo/exceptions.py,sha256=sPQjj1M5wcZxr5m0a9tRUh5B6iF1-JbiAC436hX8swI,3382 | |||
cozmo/faces.py,sha256=ZRjL9B_u974uUkMbmS4Bbx9JGVOLsZ6z0Ue5OBP4Z0I,17356 | |||
cozmo/lights.py,sha256=ICuKMv2CrtA-vvGWtEiDBhD3zbwfsEkufGoP8tVhReA,6945 | |||
cozmo/nav_memory_map.py,sha256=rJS5ylI0YmeARmvcRjdJgJHvtRDI8FrHZ47m6kE0KmE,12361 | |||
cozmo/objects.py,sha256=gTCYvkan6YLKHaFW10l7uDZMVEOawoZpIwXqkkCPxAA,36156 | |||
cozmo/oled_face.py,sha256=INkcT0JYbP5MfHMa4kZbFxfgLwzn-jCfuzpMKxpUu2Y,5228 | |||
cozmo/opengl.py,sha256=lTId8lLcls18JPL24scyghaKTVMAhi9s_TFykVmnGU0,59378 | |||
cozmo/pets.py,sha256=xREwNU3Leq4B207-h89kIo4q_wSswgWcAeMG1r9mmKY,6446 | |||
cozmo/robot.py,sha256=Cxdn1zuvEIsaBrj9shH9rwn0CcqQcyVbmig7JGSNFTw,104347 | |||
cozmo/robot_alignment.py,sha256=zjrZfd-a6b-_NE8agKC5kBVjhx9WhLRAXQOgGeccjMI,1722 | |||
cozmo/run.py,sha256=JFksm2Zlrb5Cc1H8xTo-OOfwFeh1vhV1ezcE_v9VDkc,34333 | |||
cozmo/song.py,sha256=TDVWjqGtirWHcU5OneWumOWDsrw70dZ08s5DZj6H4XE,4096 | |||
cozmo/tkview.py,sha256=iDjcSbPAEOk-H4dyzEdju0EjKUxDzBSIigYNQYwrupE,5480 | |||
cozmo/util.py,sha256=WzYuT5ZMofNVBDPFUl2EkESct6yHglRo-seLIbHByLI,35350 | |||
cozmo/version.py,sha256=RRMB8PZ3nosD8JyOFz7yrH2eunw9ZdMQbqm_O3ghTyE,994 | |||
cozmo/world.py,sha256=HLQpFvQPFSP3R2GiawsXWPdrH8_mW9fYOVmf21LnYpA,51745 | |||
cozmo/assets/LICENSE.txt,sha256=Pal4SZLGFSDAQsf_BIEfhIryYHphbdX6CXlvNzIoFko,6211 | |||
cozmo/assets/cozmo.mtl,sha256=qplYD-9bnluRy-Nb0-6boK3_o-dFtN-UC1GyD1aA7aM,922 | |||
cozmo/assets/cozmo.obj,sha256=JjLdx6On5oGMnQn3XQvwVb8pPpUJCgiPcAsPbrjMLF8,418388 | |||
cozmo/assets/cube.obj,sha256=uzM98bsfoptt6IpINTSkDbDuW-mOf6GG-66R6u59Htc,27246 | |||
cozmo/assets/cube1.jpg,sha256=xMTp74T7PWf6BHLQGNwnZfCGaDcIvPSpnOxogl_TN6Q,36785 | |||
cozmo/assets/cube1.mtl,sha256=dPiG9-BmeLpk5MGjwoJ1FIfz6K-xxIpn8lvX1SQnRfY,127 | |||
cozmo/assets/cube2.jpg,sha256=2SckIUYkvdYzUQYmllNROmro3qZakVD3tc9cxFmhvVk,36852 | |||
cozmo/assets/cube2.mtl,sha256=1koNkJAPWrxABA8oe_rNT_nHUE1tIx9nPPuIjK9gGHw,127 | |||
cozmo/assets/cube3.jpg,sha256=n0JJa5Vr9P6ZqJ88TbVH4pHCuuKRACNBXLQYCOkTmtE,37652 | |||
cozmo/assets/cube3.mtl,sha256=CSqFZhKNlW-IMsPCYmYo9UTEetznkUhQ44rgkk9_4-c,127 | |||
cozmo/usbmux/__init__.py,sha256=kcvhyDtwVb3dWaFwmyhISp1VUzYinjaHrYJOZoge22s,628 | |||
cozmo/usbmux/usbmux.py,sha256=Piv6vRfWHtOLNhC50XT_Oe3vF7FHWffvAhXwP07CdQc,17161 | |||
cozmo-1.4.10.dist-info/LICENSE.txt,sha256=THPe12iw4yd8mbwjmhwsGiCsmC2QDopJnGNSbSh9hkc,10356 | |||
cozmo-1.4.10.dist-info/METADATA,sha256=aXWZywcKBSM1Dk6slfL9nYzZ59700vG4Skc3m38j5Qg,1914 | |||
cozmo-1.4.10.dist-info/WHEEL,sha256=U88EhGIw8Sj2_phqajeu_EAi3RAo8-C6zV3REsWbWbs,92 | |||
cozmo-1.4.10.dist-info/top_level.txt,sha256=N7B-F2_miE1pnrFjxvVoCSTGOaBYufBSIsm6LZ6GdrM,6 | |||
cozmo-1.4.10.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1 | |||
cozmo-1.4.10.dist-info/RECORD,, | |||
cozmo-1.4.10.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 | |||
cozmo/usbmux/__pycache__/usbmux.cpython-36.pyc,, | |||
cozmo/usbmux/__pycache__/__init__.cpython-36.pyc,, | |||
cozmo/__pycache__/action.cpython-36.pyc,, | |||
cozmo/__pycache__/anim.cpython-36.pyc,, | |||
cozmo/__pycache__/annotate.cpython-36.pyc,, | |||
cozmo/__pycache__/audio.cpython-36.pyc,, | |||
cozmo/__pycache__/base.cpython-36.pyc,, | |||
cozmo/__pycache__/behavior.cpython-36.pyc,, | |||
cozmo/__pycache__/camera.cpython-36.pyc,, | |||
cozmo/__pycache__/clad_protocol.cpython-36.pyc,, | |||
cozmo/__pycache__/conn.cpython-36.pyc,, | |||
cozmo/__pycache__/event.cpython-36.pyc,, | |||
cozmo/__pycache__/exceptions.cpython-36.pyc,, | |||
cozmo/__pycache__/faces.cpython-36.pyc,, | |||
cozmo/__pycache__/lights.cpython-36.pyc,, | |||
cozmo/__pycache__/nav_memory_map.cpython-36.pyc,, | |||
cozmo/__pycache__/objects.cpython-36.pyc,, | |||
cozmo/__pycache__/oled_face.cpython-36.pyc,, | |||
cozmo/__pycache__/opengl.cpython-36.pyc,, | |||
cozmo/__pycache__/pets.cpython-36.pyc,, | |||
cozmo/__pycache__/robot.cpython-36.pyc,, | |||
cozmo/__pycache__/robot_alignment.cpython-36.pyc,, | |||
cozmo/__pycache__/run.cpython-36.pyc,, | |||
cozmo/__pycache__/song.cpython-36.pyc,, | |||
cozmo/__pycache__/tkview.cpython-36.pyc,, | |||
cozmo/__pycache__/util.cpython-36.pyc,, | |||
cozmo/__pycache__/version.cpython-36.pyc,, | |||
cozmo/__pycache__/world.cpython-36.pyc,, | |||
cozmo/__pycache__/_clad.cpython-36.pyc,, | |||
cozmo/__pycache__/__init__.cpython-36.pyc,, |
@@ -0,0 +1,5 @@ | |||
Wheel-Version: 1.0 | |||
Generator: bdist_wheel (0.33.1) | |||
Root-Is-Purelib: true | |||
Tag: py3-none-any | |||
@@ -0,0 +1 @@ | |||
cozmo |
@@ -0,0 +1 @@ | |||
@@ -0,0 +1,180 @@ | |||
Unless otherwise stated in that file, or the folder containing that file, all | |||
files in the Cozmo SDK are Copyright (c) 2016-2017 Anki Inc. and licensed under | |||
the Apache 2.0 License: | |||
Apache License | |||
Version 2.0, January 2004 | |||
http://www.apache.org/licenses/ | |||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION | |||
1. Definitions. | |||
"License" shall mean the terms and conditions for use, reproduction, | |||
and distribution as defined by Sections 1 through 9 of this document. | |||
"Licensor" shall mean the copyright owner or entity authorized by | |||
the copyright owner that is granting the License. | |||
"Legal Entity" shall mean the union of the acting entity and all | |||
other entities that control, are controlled by, or are under common | |||
control with that entity. For the purposes of this definition, | |||
"control" means (i) the power, direct or indirect, to cause the | |||
direction or management of such entity, whether by contract or | |||
otherwise, or (ii) ownership of fifty percent (50%) or more of the | |||
outstanding shares, or (iii) beneficial ownership of such entity. | |||
"You" (or "Your") shall mean an individual or Legal Entity | |||
exercising permissions granted by this License. | |||
"Source" form shall mean the preferred form for making modifications, | |||
including but not limited to software source code, documentation | |||
source, and configuration files. | |||
"Object" form shall mean any form resulting from mechanical | |||
transformation or translation of a Source form, including but | |||
not limited to compiled object code, generated documentation, | |||
and conversions to other media types. | |||
"Work" shall mean the work of authorship, whether in Source or | |||
Object form, made available under the License, as indicated by a | |||
copyright notice that is included in or attached to the work | |||
(an example is provided in the Appendix below). | |||
"Derivative Works" shall mean any work, whether in Source or Object | |||
form, that is based on (or derived from) the Work and for which the | |||
editorial revisions, annotations, elaborations, or other modifications | |||
represent, as a whole, an original work of authorship. For the purposes | |||
of this License, Derivative Works shall not include works that remain | |||
separable from, or merely link (or bind by name) to the interfaces of, | |||
the Work and Derivative Works thereof. | |||
"Contribution" shall mean any work of authorship, including | |||
the original version of the Work and any modifications or additions | |||
to that Work or Derivative Works thereof, that is intentionally | |||
submitted to Licensor for inclusion in the Work by the copyright owner | |||
or by an individual or Legal Entity authorized to submit on behalf of | |||
the copyright owner. For the purposes of this definition, "submitted" | |||
means any form of electronic, verbal, or written communication sent | |||
to the Licensor or its representatives, including but not limited to | |||
communication on electronic mailing lists, source code control systems, | |||
and issue tracking systems that are managed by, or on behalf of, the | |||
Licensor for the purpose of discussing and improving the Work, but | |||
excluding communication that is conspicuously marked or otherwise | |||
designated in writing by the copyright owner as "Not a Contribution." | |||
"Contributor" shall mean Licensor and any individual or Legal Entity | |||
on behalf of whom a Contribution has been received by Licensor and | |||
subsequently incorporated within the Work. | |||
2. Grant of Copyright License. Subject to the terms and conditions of | |||
this License, each Contributor hereby grants to You a perpetual, | |||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable | |||
copyright license to reproduce, prepare Derivative Works of, | |||
publicly display, publicly perform, sublicense, and distribute the | |||
Work and such Derivative Works in Source or Object form. | |||
3. Grant of Patent License. Subject to the terms and conditions of | |||
this License, each Contributor hereby grants to You a perpetual, | |||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable | |||
(except as stated in this section) patent license to make, have made, | |||
use, offer to sell, sell, import, and otherwise transfer the Work, | |||
where such license applies only to those patent claims licensable | |||
by such Contributor that are necessarily infringed by their | |||
Contribution(s) alone or by combination of their Contribution(s) | |||
with the Work to which such Contribution(s) was submitted. If You | |||
institute patent litigation against any entity (including a | |||
cross-claim or counterclaim in a lawsuit) alleging that the Work | |||
or a Contribution incorporated within the Work constitutes direct | |||
or contributory patent infringement, then any patent licenses | |||
granted to You under this License for that Work shall terminate | |||
as of the date such litigation is filed. | |||
4. Redistribution. You may reproduce and distribute copies of the | |||
Work or Derivative Works thereof in any medium, with or without | |||
modifications, and in Source or Object form, provided that You | |||
meet the following conditions: | |||
(a) You must give any other recipients of the Work or | |||
Derivative Works a copy of this License; and | |||
(b) You must cause any modified files to carry prominent notices | |||
stating that You changed the files; and | |||
(c) You must retain, in the Source form of any Derivative Works | |||
that You distribute, all copyright, patent, trademark, and | |||
attribution notices from the Source form of the Work, | |||
excluding those notices that do not pertain to any part of | |||
the Derivative Works; and | |||
(d) If the Work includes a "NOTICE" text file as part of its | |||
distribution, then any Derivative Works that You distribute must | |||
include a readable copy of the attribution notices contained | |||
within such NOTICE file, excluding those notices that do not | |||
pertain to any part of the Derivative Works, in at least one | |||
of the following places: within a NOTICE text file distributed | |||
as part of the Derivative Works; within the Source form or | |||
documentation, if provided along with the Derivative Works; or, | |||
within a display generated by the Derivative Works, if and | |||
wherever such third-party notices normally appear. The contents | |||
of the NOTICE file are for informational purposes only and | |||
do not modify the License. You may add Your own attribution | |||
notices within Derivative Works that You distribute, alongside | |||
or as an addendum to the NOTICE text from the Work, provided | |||
that such additional attribution notices cannot be construed | |||
as modifying the License. | |||
You may add Your own copyright statement to Your modifications and | |||
may provide additional or different license terms and conditions | |||
for use, reproduction, or distribution of Your modifications, or | |||
for any such Derivative Works as a whole, provided Your use, | |||
reproduction, and distribution of the Work otherwise complies with | |||
the conditions stated in this License. | |||
5. Submission of Contributions. Unless You explicitly state otherwise, | |||
any Contribution intentionally submitted for inclusion in the Work | |||
by You to the Licensor shall be under the terms and conditions of | |||
this License, without any additional terms or conditions. | |||
Notwithstanding the above, nothing herein shall supersede or modify | |||
the terms of any separate license agreement you may have executed | |||
with Licensor regarding such Contributions. | |||
6. Trademarks. This License does not grant permission to use the trade | |||
names, trademarks, service marks, or product names of the Licensor, | |||
except as required for reasonable and customary use in describing the | |||
origin of the Work and reproducing the content of the NOTICE file. | |||
7. Disclaimer of Warranty. Unless required by applicable law or | |||
agreed to in writing, Licensor provides the Work (and each | |||
Contributor provides its Contributions) on an "AS IS" BASIS, | |||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | |||
implied, including, without limitation, any warranties or conditions | |||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A | |||
PARTICULAR PURPOSE. You are solely responsible for determining the | |||
appropriateness of using or redistributing the Work and assume any | |||
risks associated with Your exercise of permissions under this License. | |||
8. Limitation of Liability. In no event and under no legal theory, | |||
whether in tort (including negligence), contract, or otherwise, | |||
unless required by applicable law (such as deliberate and grossly | |||
negligent acts) or agreed to in writing, shall any Contributor be | |||
liable to You for damages, including any direct, indirect, special, | |||
incidental, or consequential damages of any character arising as a | |||
result of this License or out of the use or inability to use the | |||
Work (including but not limited to damages for loss of goodwill, | |||
work stoppage, computer failure or malfunction, or any and all | |||
other commercial damages or losses), even if such Contributor | |||
has been advised of the possibility of such damages. | |||
9. Accepting Warranty or Additional Liability. While redistributing | |||
the Work or Derivative Works thereof, You may choose to offer, | |||
and charge a fee for, acceptance of support, warranty, indemnity, | |||
or other liability obligations and/or rights consistent with this | |||
License. However, in accepting such obligations, You may act only | |||
on Your own behalf and on Your sole responsibility, not on behalf | |||
of any other Contributor, and only if You agree to indemnify, | |||
defend, and hold each Contributor harmless for any liability | |||
incurred by, or claims asserted against, such Contributor by reason | |||
of your accepting any such warranty or additional liability. | |||
END OF TERMS AND CONDITIONS |
@@ -0,0 +1,86 @@ | |||
# Copyright (c) 2016 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. | |||
import sys | |||
if sys.version_info < (3,5,1): | |||
sys.exit('cozmo requires Python 3.5.1 or later') | |||
# Verify cozmoclad version before any other imports, so we can catch a mismatch | |||
# before triggering any exceptions from missing clad definitions | |||
try: | |||
from cozmoclad import __build_version__ as __installed_cozmoclad_build_version__ | |||
except ImportError as e: | |||
sys.exit("%s\nCannot import from cozmoclad: Do `pip3 install --user cozmoclad` to install" % e) | |||
from .version import __version__, __cozmoclad_version__, __min_cozmoclad_version__ | |||
def verify_min_clad_version(): | |||
def _make_sortable_version_string(ver_string): | |||
# pad out an x.y.z version to a 5.5.5 string with leading zeroes | |||
ver_elements = [str(int(x)).zfill(5) for x in ver_string.split(".")] | |||
return '.'.join(ver_elements) | |||
def _trimmed_version(ver_string): | |||
# Trim leading zeros from the version string | |||
trimmed_parts = [str(int(x)) for x in ver_string.split(".")] | |||
return '.'.join(trimmed_parts) | |||
min_cozmoclad_version_str = _make_sortable_version_string(__min_cozmoclad_version__) | |||
if __installed_cozmoclad_build_version__ < min_cozmoclad_version_str: | |||
sys.exit("Incompatible cozmoclad version %s for SDK %s - needs at least %s\n" | |||
"Do `pip3 install --user --upgrade cozmoclad` to upgrade" % ( | |||
_trimmed_version(__installed_cozmoclad_build_version__), | |||
__version__, | |||
__min_cozmoclad_version__)) | |||
verify_min_clad_version() | |||
import logging as _logging | |||
#: The general purpose logger logs high level information about Cozmo events. | |||
logger = _logging.getLogger('cozmo.general') | |||
#: The protocol logger logs low level messages that are sent back and forth to Cozmo. | |||
logger_protocol = _logging.getLogger('cozmo.protocol') | |||
del _logging | |||
from . import action | |||
from . import anim | |||
from . import annotate | |||
from . import behavior | |||
from . import conn | |||
from . import event | |||
from . import exceptions | |||
from . import lights | |||
from . import nav_memory_map | |||
from . import objects | |||
from . import oled_face | |||
from . import robot | |||
from . import robot_alignment | |||
from . import run | |||
from . import util | |||
from . import world | |||
from .exceptions import * | |||
from .run import * | |||
__all__ = ['logger', 'logger_protocol', | |||
'action', 'anim', 'annotate', 'behavior', 'conn', 'event', | |||
'exceptions', 'lights', 'objects', 'oled_face', 'nav_memory_map', | |||
'robot', 'robot_alignment', 'run', 'util', 'world'] + \ | |||
(run.__all__ + exceptions.__all__) |
@@ -0,0 +1,153 @@ | |||
# 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. | |||
__all__ = ['CladEnumWrapper'] | |||
import sys | |||
from . import event | |||
from . import logger | |||
from cozmoclad.clad.externalInterface import messageEngineToGame as messageEngineToGame | |||
from cozmoclad.clad.externalInterface import messageGameToEngine as messageGameToEngine | |||
# Shortcut access to CLAD classes | |||
_clad_to_engine_anki = messageGameToEngine.Anki | |||
_clad_to_engine_cozmo = messageGameToEngine.Anki.Cozmo | |||
_clad_to_engine_iface = messageGameToEngine.Anki.Cozmo.ExternalInterface | |||
_clad_to_game_anki = messageEngineToGame.Anki | |||
_clad_to_game_cozmo = messageEngineToGame.Anki.Cozmo | |||
_clad_to_game_iface = messageEngineToGame.Anki.Cozmo.ExternalInterface | |||
# Register event types for engine to game messages | |||
# (e.g. _MsgObjectMoved) | |||
for _name in vars(_clad_to_game_iface.MessageEngineToGame.Tag): | |||
attrs = { | |||
'__doc__': 'Internal protocol message', | |||
'msg': 'Message data' | |||
} | |||
_name = '_Msg' + _name | |||
cls = event._register_dynamic_event_type(_name, attrs) | |||
globals()[_name] = cls | |||
def _all_caps_to_pascal_case(name): | |||
# Convert a string from CAPS_CASE_WORDS to PascalCase (e.g. CapsCaseWords) | |||
ret_str = "" | |||
first_char = True | |||
# Build the return string | |||
for char in name: | |||
if char == "_": | |||
# skip underscores, but reset that next char will be start of a new word | |||
first_char = True | |||
else: | |||
# First letter of a word is uppercase, rest are lowercase | |||
if first_char: | |||
ret_str += char.upper() | |||
first_char = False | |||
else: | |||
ret_str += char.lower() | |||
return ret_str | |||
class CladEnumWrapper: | |||
"""Subclass this for an easy way to wrap a clad-enum in a documentable class. | |||
Call cls._init_class() after declaration of the sub-class to verify the | |||
type after construction and set up id to type mapping. | |||
""" | |||
# Override this to the CLAD enum type being wrapped | |||
_clad_enum = None | |||
# Override this with the type used for each instance | |||
# e.g. collections.namedtuple('_ClassName', 'name id') | |||
_entry_type = None | |||
_id_to_entry_type = None # type: dict | |||
@classmethod | |||
def find_by_id(cls, id): | |||
return cls._id_to_entry_type.get(id) | |||
@classmethod | |||
def _verify(cls, warn_on_missing_definitions=True, add_missing_definitions=True): | |||
"""Verify that definitions are in sync with the underlying CLAD values. | |||
Optionally also warn about and/or add any missing definitions. | |||
Args: | |||
warn_on_missing_definitions (bool): True to warn about any entries | |||
in the underlying CLAD enum that haven't been explicitly | |||
declared (includes suggested format for adding, which can then | |||
be documented with `#:` comments for the generated docs. | |||
add_missing_definitions (bool): True to automatically add any | |||
entries in the underlying CLAD enum that haven't been explicitly | |||
declared. Note that these definitions will work at runtime, but | |||
won't be present in the auto-generated docs. | |||
""" | |||
missing_definitions_message = None | |||
for (_name, _id) in cls._clad_enum.__dict__.items(): | |||
# Ignore any private entries (or internal Python objects) and any | |||
# "Count" entries in the enum | |||
if not _name.startswith('_') and (_name != 'Count') and _id >= 0: | |||
attr = getattr(cls, _name, None) | |||
if attr is None: | |||
# Try valid, but less common, alternatives of the name - | |||
# leading underscores for private vars, and/or PascalCase | |||
# when the Clad type is in CAPS_CASE | |||
alternative_names = ["_" + _name] | |||
is_upper_case = _name == _name.upper() | |||
if is_upper_case: | |||
pascal_case_name = _all_caps_to_pascal_case(_name) | |||
alternative_names.extend([pascal_case_name, | |||
"_" + pascal_case_name]) | |||
alternative_names.append(_name.replace("_","")) | |||
for alt_name in alternative_names: | |||
attr = getattr(cls, alt_name, None) | |||
if attr is not None: | |||
break | |||
if attr is not None: | |||
if attr.id != _id: | |||
sys.exit( | |||
'Incorrect definition in %s for id %s=%s, (should =%s) - line should read:\n' | |||
'%s = _entry_type("%s", _clad_enum.%s)' | |||
% (str(cls), _name, attr.id, _id, _name, _name, _name)) | |||
else: | |||
if warn_on_missing_definitions: | |||
if missing_definitions_message is None: | |||
missing_definitions_message = ('Missing definition(s) in %s - to document them add:' % str(cls)) | |||
missing_definitions_message += ('\n %s = _entry_type("%s", _clad_enum.%s)' % (_name, _name, _name)) | |||
if is_upper_case: | |||
missing_definitions_message += ('\n or %s = _entry_type("%s", _clad_enum.%s)' % (pascal_case_name, pascal_case_name, _name)) | |||
if add_missing_definitions: | |||
setattr(cls, _name, cls._entry_type(_name, _id)) | |||
if missing_definitions_message is not None: | |||
logger.warning(missing_definitions_message) | |||
@classmethod | |||
def _build_id_to_entry_type(cls): | |||
# populate _id_to_entry_type mapping | |||
cls._id_to_entry_type = dict() | |||
for (_name, _entry) in cls.__dict__.items(): | |||
if isinstance(_entry, cls._entry_type): | |||
cls._id_to_entry_type[_entry.id] = _entry | |||
@classmethod | |||
def _init_class(cls, warn_on_missing_definitions=True, add_missing_definitions=True): | |||
cls._verify(warn_on_missing_definitions, add_missing_definitions) | |||
cls._build_id_to_entry_type() |
@@ -0,0 +1,667 @@ | |||
# 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. | |||
''' | |||
Actions encapsulate specific high-level tasks that the Cozmo robot can perform. | |||
They have a definite beginning and end. | |||
These tasks include picking up an object, rotating in place, saying text, etc. | |||
Actions are usually triggered by a call to a method on the | |||
:class:`cozmo.robot.Robot` class such as :meth:`~cozmo.robot.Robot.turn_in_place` | |||
The call will return an object that subclasses :class:`Action` that can be | |||
used to cancel the action, or be observed to wait or be notified when the | |||
action completes (or fails) by calling its | |||
:meth:`~cozmo.event.Dispatcher.wait_for` or | |||
:meth:`~cozmo.event.Dispatcher.add_event_handler` methods. | |||
Warning: | |||
Unless you pass ``in_parallel=True`` when starting the action, no other | |||
action can be active at the same time. Attempting to trigger a non-parallel | |||
action when another action is already in progress will result in a | |||
:class:`~cozmo.exceptions.RobotBusy` exception being raised. | |||
When using ``in_parallel=True`` you may see an action fail with the result | |||
:attr:`ActionResults.TRACKS_LOCKED` - this indicates that another in-progress | |||
action has already locked that movement track (e.g. two actions cannot | |||
move the head at the same time). | |||
''' | |||
# __all__ should order by constants, event classes, other classes, functions. | |||
__all__ = ['ACTION_IDLE', 'ACTION_RUNNING', 'ACTION_SUCCEEDED', | |||
'ACTION_FAILED', 'ACTION_ABORTING', | |||
'EvtActionStarted', 'EvtActionCompleted', 'Action', 'ActionResults'] | |||
from collections import namedtuple | |||
import sys | |||
from . import logger | |||
from . import event | |||
from . import exceptions | |||
from ._clad import _clad_to_engine_iface, _clad_to_engine_cozmo, _clad_to_game_cozmo, CladEnumWrapper | |||
#: string: Action idle state | |||
ACTION_IDLE = 'action_idle' | |||
#: string: Action running state | |||
ACTION_RUNNING = 'action_running' | |||
#: string: Action succeeded state | |||
ACTION_SUCCEEDED = 'action_succeeded' | |||
#: string: Action failed state | |||
ACTION_FAILED = 'action_failed' | |||
#: string: Action failed state | |||
ACTION_ABORTING = 'action_aborting' | |||
_VALID_STATES = {ACTION_IDLE, ACTION_RUNNING, ACTION_SUCCEEDED, ACTION_FAILED, ACTION_ABORTING} | |||
class _ActionResult(namedtuple('_ActionResult', 'name id')): | |||
# Tuple mapping between CLAD ActionResult name and ID | |||
# All instances will be members of ActionResults | |||
# Keep _ActionResult as lightweight as a normal namedtuple | |||
__slots__ = () | |||
def __str__(self): | |||
return 'ActionResults.%s' % self.name | |||
class ActionResults(CladEnumWrapper): | |||
"""The possible result values for an Action. | |||
An Action's result is set when the action completes. | |||
""" | |||
_clad_enum = _clad_to_game_cozmo.ActionResult | |||
_entry_type = _ActionResult | |||
#: Action completed successfully. | |||
SUCCESS = _ActionResult("SUCCESS", _clad_enum.SUCCESS) | |||
#: Action is still running. | |||
RUNNING = _ActionResult("RUNNING", _clad_enum.RUNNING) | |||
#: Action was cancelled (e.g. via :meth:`~cozmo.robot.Robot.abort_all_actions` or | |||
#: :meth:`Action.abort`). | |||
CANCELLED_WHILE_RUNNING = _ActionResult("CANCELLED_WHILE_RUNNING", _clad_enum.CANCELLED_WHILE_RUNNING) | |||
#: Action aborted itself (e.g. had invalid attributes, or a runtime failure). | |||
ABORT = _ActionResult("ABORT", _clad_enum.ABORT) | |||
#: Animation Action aborted itself (e.g. there was an error playing the animation). | |||
ANIM_ABORTED = _ActionResult("ANIM_ABORTED", _clad_enum.ANIM_ABORTED) | |||
#: There was an error related to vision markers. | |||
BAD_MARKER = _ActionResult("BAD_MARKER", _clad_enum.BAD_MARKER) | |||
# (Undocumented) There was a problem related to a subscribed or unsupported message tag (indicates bug in engine) | |||
BAD_MESSAGE_TAG = _ActionResult("BAD_MESSAGE_TAG", _clad_enum.BAD_MESSAGE_TAG) | |||
#: There was a problem with the Object ID provided (e.g. there is no Object with that ID). | |||
BAD_OBJECT = _ActionResult("BAD_OBJECT", _clad_enum.BAD_OBJECT) | |||
#: There was a problem with the Pose provided. | |||
BAD_POSE = _ActionResult("BAD_POSE", _clad_enum.BAD_POSE) | |||
# (Undocumented) The SDK-provided tag was bad (shouldn't occur - would indicate a bug in the SDK) | |||
BAD_TAG = _ActionResult("BAD_TAG", _clad_enum.BAD_TAG) | |||
# (Undocumented) Shouldn't occur outside of factory | |||
FAILED_SETTING_CALIBRATION = _ActionResult("FAILED_SETTING_CALIBRATION", _clad_enum.FAILED_SETTING_CALIBRATION) | |||
#: There was an error following the planned path. | |||
FOLLOWING_PATH_BUT_NOT_TRAVERSING = _ActionResult("FOLLOWING_PATH_BUT_NOT_TRAVERSING", _clad_enum.FOLLOWING_PATH_BUT_NOT_TRAVERSING) | |||
#: The action was interrupted by another Action or Behavior. | |||
INTERRUPTED = _ActionResult("INTERRUPTED", _clad_enum.INTERRUPTED) | |||
#: The robot ended up in an "off treads state" not valid for this action (e.g. | |||
#: the robot was placed on its back while executing a turn) | |||
INVALID_OFF_TREADS_STATE = _ActionResult("INVALID_OFF_TREADS_STATE", | |||
_clad_to_game_cozmo.ActionResult.INVALID_OFF_TREADS_STATE) | |||
#: The Up Axis of a carried object doesn't match the desired placement pose. | |||
MISMATCHED_UP_AXIS = _ActionResult("MISMATCHED_UP_AXIS", _clad_enum.MISMATCHED_UP_AXIS) | |||
#: No valid Animation name was found. | |||
NO_ANIM_NAME = _ActionResult("NO_ANIM_NAME", _clad_enum.NO_ANIM_NAME) | |||
#: An invalid distance value was given. | |||
NO_DISTANCE_SET = _ActionResult("NO_DISTANCE_SET", _clad_enum.NO_DISTANCE_SET) | |||
#: There was a problem with the Face ID (e.g. Cozmo doesn't no where it is). | |||
NO_FACE = _ActionResult("NO_FACE", _clad_enum.NO_FACE) | |||
#: No goal pose was set. | |||
NO_GOAL_SET = _ActionResult("NO_GOAL_SET", _clad_enum.NO_GOAL_SET) | |||
#: No pre-action poses were found (e.g. could not get into position). | |||
NO_PREACTION_POSES = _ActionResult("NO_PREACTION_POSES", _clad_enum.NO_PREACTION_POSES) | |||
#: No object is being carried, but the action requires one. | |||
NOT_CARRYING_OBJECT_ABORT = _ActionResult("NOT_CARRYING_OBJECT_ABORT", _clad_enum.NOT_CARRYING_OBJECT_ABORT) | |||
#: Initial state of an Action to indicate it has not yet started. | |||
NOT_STARTED = _ActionResult("NOT_STARTED", _clad_enum.NOT_STARTED) | |||
#: No sub-action was provided. | |||
NULL_SUBACTION = _ActionResult("NULL_SUBACTION", _clad_enum.NULL_SUBACTION) | |||
#: Cozmo was unable to plan a path. | |||
PATH_PLANNING_FAILED_ABORT = _ActionResult("PATH_PLANNING_FAILED_ABORT", _clad_enum.PATH_PLANNING_FAILED_ABORT) | |||
#: The object that Cozmo is attempting to pickup is unexpectedly moving (e.g | |||
#: it is being moved by someone else). | |||
PICKUP_OBJECT_UNEXPECTEDLY_MOVING = _ActionResult("PICKUP_OBJECT_UNEXPECTEDLY_MOVING", _clad_enum.PICKUP_OBJECT_UNEXPECTEDLY_MOVING) | |||
#: The object that Cozmo thought he was lifting didn't start moving, so he | |||
#: must have missed. | |||
PICKUP_OBJECT_UNEXPECTEDLY_NOT_MOVING = _ActionResult("PICKUP_OBJECT_UNEXPECTEDLY_NOT_MOVING", _clad_enum.PICKUP_OBJECT_UNEXPECTEDLY_NOT_MOVING) | |||
# (Undocumented) Shouldn't occur in SDK usage | |||
SEND_MESSAGE_TO_ROBOT_FAILED = _ActionResult("SEND_MESSAGE_TO_ROBOT_FAILED", _clad_enum.SEND_MESSAGE_TO_ROBOT_FAILED) | |||
#: Cozmo is unexpectedly still carrying an object. | |||
STILL_CARRYING_OBJECT = _ActionResult("STILL_CARRYING_OBJECT", _clad_enum.STILL_CARRYING_OBJECT) | |||
#: The Action timed out before completing correctly. | |||
TIMEOUT = _ActionResult("TIMEOUT", _clad_enum.TIMEOUT) | |||
#: One or more animation tracks (Head, Lift, Body, Face, Backpack Lights, Audio) | |||
#: are already being used by another Action. | |||
TRACKS_LOCKED = _ActionResult("TRACKS_LOCKED", _clad_enum.TRACKS_LOCKED) | |||
#: There was an internal error related to an unexpected type of dock action. | |||
UNEXPECTED_DOCK_ACTION = _ActionResult("UNEXPECTED_DOCK_ACTION", _clad_enum.UNEXPECTED_DOCK_ACTION) | |||
# (Undocumented) Shouldn't occur outside of factory. | |||
UNKNOWN_TOOL_CODE = _ActionResult("UNKNOWN_TOOL_CODE", _clad_enum.UNKNOWN_TOOL_CODE) | |||
# (Undocumented) There was a problem in the subclass's update. | |||
UPDATE_DERIVED_FAILED = _ActionResult("UPDATE_DERIVED_FAILED", _clad_enum.UPDATE_DERIVED_FAILED) | |||
#: Cozmo did not see the expected result (e.g. unable to see cubes in their | |||
#: expected position after a related action). | |||
VISUAL_OBSERVATION_FAILED = _ActionResult("VISUAL_OBSERVATION_FAILED", _clad_enum.VISUAL_OBSERVATION_FAILED) | |||
#: The Action failed, but may succeed if retried. | |||
RETRY = _ActionResult("RETRY", _clad_enum.RETRY) | |||
#: Failed to get into position. | |||
DID_NOT_REACH_PREACTION_POSE = _ActionResult("DID_NOT_REACH_PREACTION_POSE", _clad_enum.DID_NOT_REACH_PREACTION_POSE) | |||
#: Failed to follow the planned path. | |||
FAILED_TRAVERSING_PATH = _ActionResult("FAILED_TRAVERSING_PATH", _clad_enum.FAILED_TRAVERSING_PATH) | |||
#: The previous attempt to pick and place an object failed. | |||
LAST_PICK_AND_PLACE_FAILED = _ActionResult("LAST_PICK_AND_PLACE_FAILED", _clad_enum.LAST_PICK_AND_PLACE_FAILED) | |||
#: The required motor isn't moving so the action cannot complete. | |||
MOTOR_STOPPED_MAKING_PROGRESS = _ActionResult("MOTOR_STOPPED_MAKING_PROGRESS", _clad_enum.MOTOR_STOPPED_MAKING_PROGRESS) | |||
#: Not carrying an object when it was expected, but may succeed if the action is retried. | |||
NOT_CARRYING_OBJECT_RETRY = _ActionResult("NOT_CARRYING_OBJECT_RETRY", _clad_enum.NOT_CARRYING_OBJECT_RETRY) | |||
#: Cozmo is expected to be on the charger, but is not. | |||
NOT_ON_CHARGER = _ActionResult("NOT_ON_CHARGER", _clad_enum.NOT_ON_CHARGER) | |||
#: Cozmo was unable to plan a path, but may succeed if the action is retried. | |||
PATH_PLANNING_FAILED_RETRY = _ActionResult("PATH_PLANNING_FAILED_RETRY", _clad_enum.PATH_PLANNING_FAILED_RETRY) | |||
#: There is no room to place the object at the desired destination. | |||
PLACEMENT_GOAL_NOT_FREE = _ActionResult("PLACEMENT_GOAL_NOT_FREE", _clad_enum.PLACEMENT_GOAL_NOT_FREE) | |||
#: Cozmo failed to drive off the charger. | |||
STILL_ON_CHARGER = _ActionResult("STILL_ON_CHARGER", _clad_enum.STILL_ON_CHARGER) | |||
#: Cozmo's pitch is at an unexpected angle for the Action. | |||
UNEXPECTED_PITCH_ANGLE = _ActionResult("UNEXPECTED_PITCH_ANGLE", _clad_enum.UNEXPECTED_PITCH_ANGLE) | |||
ActionResults._init_class() | |||
class EvtActionStarted(event.Event): | |||
'''Triggered when a robot starts an action.''' | |||
action = "The action that started" | |||
class EvtActionCompleted(event.Event): | |||
'''Triggered when a robot action has completed or failed.''' | |||
action = "The action that completed" | |||
state = 'The state of the action; either cozmo.action.ACTION_SUCCEEDED or cozmo.action.ACTION_FAILED' | |||
failure_code = 'A failure code such as "cancelled"' | |||
failure_reason = 'A human-readable failure reason' | |||
class Action(event.Dispatcher): | |||
"""An action holds the state of an in-progress robot action | |||
""" | |||
# We allow sub-classes of Action to optionally disable logging messages | |||
# related to those actions being aborted - this is useful for actions | |||
# that are aborted frequently (by design) and would otherwise spam the log | |||
_enable_abort_logging = True | |||
def __init__(self, *, conn, robot, **kw): | |||
super().__init__(**kw) | |||
#: :class:`~cozmo.conn.CozmoConnection`: The connection on which the action was sent. | |||
self.conn = conn | |||
#: :class:`~cozmo.robot.Robot`: Th robot instance executing the action. | |||
self.robot = robot | |||
self._action_id = None | |||
self._state = ACTION_IDLE | |||
self._failure_code = None | |||
self._failure_reason = None | |||
self._result = None | |||
self._completed_event = None | |||
self._completed_event_pending = False | |||
def __repr__(self): | |||
extra = self._repr_values() | |||
if len(extra) > 0: | |||
extra = ' '+extra | |||
if self._state == ACTION_FAILED: | |||
extra += (" failure_reason='%s' failure_code=%s result=%s" % | |||
(self._failure_reason, self._failure_code, self.result)) | |||
return '<%s state=%s%s>' % (self.__class__.__name__, self.state, extra) | |||
def _repr_values(self): | |||
return '' | |||
def _encode(self): | |||
raise NotImplementedError() | |||
def _start(self): | |||
self._state = ACTION_RUNNING | |||
self.dispatch_event(EvtActionStarted, action=self) | |||
def _set_completed(self, msg): | |||
self._state = ACTION_SUCCEEDED | |||
self._completed_event_pending = False | |||
self._dispatch_completed_event(msg) | |||
def _dispatch_completed_event(self, msg): | |||
# Override to extra action-specific data from msg and generate | |||
# an action-specific completion event. Do not call super if overriden. | |||
# Must generate a subclass of EvtActionCompleted. | |||
self._completed_event = EvtActionCompleted(action=self, state=self._state) | |||
self.dispatch_event(self._completed_event) | |||
def _set_failed(self, code, reason): | |||
self._state = ACTION_FAILED | |||
self._failure_code = code | |||
self._failure_reason = reason | |||
self._completed_event_pending = False | |||
self._completed_event = EvtActionCompleted(action=self, state=self._state, | |||
failure_code=code, | |||
failure_reason=reason) | |||
self.dispatch_event(self._completed_event) | |||
def _set_aborting(self, log_abort_messages): | |||
if not self.is_running: | |||
raise ValueError("Action isn't currently running") | |||
if self._enable_abort_logging and log_abort_messages: | |||
logger.info('Aborting action=%s', self) | |||
self._state = ACTION_ABORTING | |||
#### Properties #### | |||
@property | |||
def is_running(self): | |||
'''bool: True if the action is currently in progress.''' | |||
return self._state == ACTION_RUNNING | |||
@property | |||
def is_completed(self): | |||
'''bool: True if the action has completed (either succeeded or failed).''' | |||
return self._state in (ACTION_SUCCEEDED, ACTION_FAILED) | |||
@property | |||
def is_aborting(self): | |||
'''bool: True if the action is aborting (will soon be either succeeded or failed).''' | |||
return self._state == ACTION_ABORTING | |||
@property | |||
def has_succeeded(self): | |||
'''bool: True if the action has succeeded.''' | |||
return self._state == ACTION_SUCCEEDED | |||
@property | |||
def has_failed(self): | |||
'''bool: True if the action has failed.''' | |||
return self._state == ACTION_FAILED | |||
@property | |||
def failure_reason(self): | |||
'''tuple of (failure_code, failure_reason): Both values will be None if no failure has occurred.''' | |||
return (self._failure_code, self._failure_reason) | |||
@property | |||
def result(self): | |||
"""An attribute of :class:`ActionResults`: The result of running the action.""" | |||
return self._result | |||
@property | |||
def state(self): | |||
'''string: The current internal state of the action as a string. | |||
Will match one of the constants: | |||
:const:`ACTION_IDLE` | |||
:const:`ACTION_RUNNING` | |||
:const:`ACTION_SUCCEEDED` | |||
:const:`ACTION_FAILED` | |||
:const:`ACTION_ABORTING` | |||
''' | |||
return self._state | |||
#### Private Event Handlers #### | |||
def _recv_msg_robot_completed_action(self, evt, *, msg): | |||
result = msg.result | |||
types = _clad_to_game_cozmo.ActionResult | |||
self._result = ActionResults.find_by_id(result) | |||
if self._result is None: | |||
logger.error("ActionResults has no entry for result id %s", result) | |||
if result == types.SUCCESS: | |||
# dispatch to the specific type to extract result info | |||
self._set_completed(msg) | |||
elif result == types.RUNNING: | |||
# XXX what does one do with this? it seems to occur after a cancel request! | |||
logger.warning('Received "running" action notification for action=%s', self) | |||
self._set_failed('running', 'Action was still running') | |||
elif result == types.NOT_STARTED: | |||
# not sure we'll see this? | |||
self._set_failed('not_started', 'Action was not started') | |||
elif result == types.TIMEOUT: | |||
self._set_failed('timeout', 'Action timed out') | |||
elif result == types.TRACKS_LOCKED: | |||
self._set_failed('tracks_locked', 'Action failed due to tracks locked') | |||
elif result == types.BAD_TAG: | |||
# guessing this is bad | |||
self._set_failed('bad_tag', 'Action failed due to bad tag') | |||
logger.error("Received FAILURE_BAD_TAG for action %s", self) | |||
elif result == types.CANCELLED_WHILE_RUNNING: | |||
self._set_failed('cancelled', 'Action was cancelled while running') | |||
elif result == types.INTERRUPTED: | |||
self._set_failed('interrupted', 'Action was interrupted') | |||
else: | |||
# All other results should fall under either the abort or retry | |||
# categories, determine the category by shifting the result | |||
result_category = result >> _clad_to_game_cozmo.ARCBitShift.NUM_BITS | |||
result_categories = _clad_to_game_cozmo.ActionResultCategory | |||
if result_category == result_categories.ABORT: | |||
self._set_failed('aborted', 'Action failed') | |||
elif result_category == result_categories.RETRY: | |||
self._set_failed('retry', 'Action failed but can be retried') | |||
else: | |||
# Shouldn't be able to get here | |||
self._set_failed('unknown', 'Action failed with unknown reason') | |||
logger.error('Received unknown action result status %s', msg) | |||
#### Public Event Handlers #### | |||
#### Commands #### | |||
def abort(self, log_abort_messages=False): | |||
'''Trigger the robot to abort the running action. | |||
Args: | |||
log_abort_messages (bool): True to log info on the action that | |||
is aborted. | |||
Raises: | |||
ValueError if the action is not currently being executed. | |||
''' | |||
self.robot._action_dispatcher._abort_action(self, log_abort_messages) | |||
async def wait_for_completed(self, timeout=None): | |||
'''Waits for the action to complete. | |||
Args: | |||
timeout (int or None): Maximum time in seconds to wait for the event. | |||
Pass None to wait indefinitely. | |||
Returns: | |||
The :class:`EvtActionCompleted` event instance | |||
Raises: | |||
:class:`asyncio.TimeoutError` | |||
''' | |||
if self.is_completed: | |||
# Already complete | |||
return self._completed_event | |||
return await self.wait_for(EvtActionCompleted, timeout=timeout) | |||
def on_completed(self, handler): | |||
'''Triggers a handler when the action completes. | |||
Args: | |||
handler (callable): An event handler which accepts arguments | |||
suited to the :class:`EvtActionCompleted` event. | |||
See :meth:`cozmo.event.add_event_handler` for more information. | |||
''' | |||
return self.add_event_handler(EvtActionCompleted, handler) | |||
class _ActionDispatcher(event.Dispatcher): | |||
_next_action_id = _clad_to_game_cozmo.ActionConstants.FIRST_SDK_TAG | |||
def __init__(self, robot, **kw): | |||
super().__init__(**kw) | |||
self.robot = robot | |||
self._in_progress = {} | |||
self._aborting = {} | |||
def _get_next_action_id(self): | |||
# Post increment _current_action_id (and loop within the SDK_TAG range) | |||
next_action_id = self.__class__._next_action_id | |||
if self.__class__._next_action_id == _clad_to_game_cozmo.ActionConstants.LAST_SDK_TAG: | |||
self.__class__._next_action_id = _clad_to_game_cozmo.ActionConstants.FIRST_SDK_TAG | |||
else: | |||
self.__class__._next_action_id += 1 | |||
return next_action_id | |||
@property | |||
def aborting_actions(self): | |||
'''generator: yields each action that is currently aborting | |||
Returns: | |||
A generator yielding :class:`cozmo.action.Action` instances | |||
''' | |||
for _, action in self._aborting.items(): | |||
yield action | |||
@property | |||
def has_in_progress_actions(self): | |||
'''bool: True if any SDK-triggered actions are still in progress.''' | |||
return len(self._in_progress) > 0 | |||
@property | |||
def in_progress_actions(self): | |||
'''generator: yields each action that is currently in progress | |||
Returns: | |||
A generator yielding :class:`cozmo.action.Action` instances | |||
''' | |||
for _, action in self._in_progress.items(): | |||
yield action | |||
async def wait_for_all_actions_completed(self): | |||
'''Waits until all actions are complete. | |||
In this case, all actions include not just in_progress actions but also | |||
include actions that we're aborting but haven't received a completed message | |||
for yet. | |||
''' | |||
while True: | |||
action = next(self.in_progress_actions, None) | |||
if action is None: | |||
action = next(self.aborting_actions, None) | |||
if action: | |||
await action.wait_for_completed() | |||
else: | |||
# all actions are now complete | |||
return | |||
def _send_single_action(self, action, in_parallel=False, num_retries=0): | |||
action_id = self._get_next_action_id() | |||
action.robot = self.robot | |||
action._action_id = action_id | |||
if self.has_in_progress_actions and not in_parallel: | |||
# Note - it doesn't matter if previous action was started as in_parallel, | |||
# starting any subsequent action with in_parallel==False will cancel | |||
# any previous actions, so we throw an exception here and require that | |||
# the client explicitly cancel or wait on earlier actions | |||
action = list(self._in_progress.values())[0] | |||
raise exceptions.RobotBusy('Robot is already performing %d action(s) %s' % | |||
(len(self._in_progress), action)) | |||
if action.is_running: | |||
raise ValueError('Action is already running') | |||
if action.is_completed: | |||
raise ValueError('Action already ran') | |||
if in_parallel: | |||
position = _clad_to_game_cozmo.QueueActionPosition.IN_PARALLEL | |||
else: | |||
position = _clad_to_game_cozmo.QueueActionPosition.NOW | |||
qmsg = _clad_to_engine_iface.QueueSingleAction( | |||
idTag=action_id, numRetries=num_retries, | |||
position=position, action=_clad_to_engine_iface.RobotActionUnion()) | |||
action_msg = action._encode() | |||
cls_name = action_msg.__class__.__name__ | |||
# For some reason, the RobotActionUnion type uses properties with a lowercase | |||
# first character, instead of uppercase like all the other unions | |||
cls_name = cls_name[0].lower() + cls_name[1:] | |||
setattr(qmsg.action, cls_name, action_msg) | |||
self.robot.conn.send_msg(qmsg) | |||
self._in_progress[action_id] = action | |||
action._start() | |||
def _is_sdk_action_id(self, action_id): | |||
return ((action_id >= _clad_to_game_cozmo.ActionConstants.FIRST_SDK_TAG) | |||
and (action_id <= _clad_to_game_cozmo.ActionConstants.LAST_SDK_TAG)) | |||
def _is_engine_action_id(self, action_id): | |||
return ((action_id >= _clad_to_game_cozmo.ActionConstants.FIRST_ENGINE_TAG) | |||
and (action_id <= _clad_to_game_cozmo.ActionConstants.LAST_ENGINE_TAG)) | |||
def _is_game_action_id(self, action_id): | |||
return ((action_id >= _clad_to_game_cozmo.ActionConstants.FIRST_GAME_TAG) | |||
and (action_id <= _clad_to_game_cozmo.ActionConstants.LAST_GAME_TAG)) | |||
def _action_id_type(self, action_id): | |||
if self._is_sdk_action_id(action_id): | |||
return "sdk" | |||
elif self._is_engine_action_id(action_id): | |||
return "engine" | |||
elif self._is_game_action_id(action_id): | |||
return "game" | |||
else: | |||
return "unknown" | |||
def _recv_msg_robot_completed_action(self, evt, *, msg): | |||
action_id = msg.idTag | |||
is_sdk_action = self._is_sdk_action_id(action_id) | |||
action = self._in_progress.get(action_id) | |||
was_aborted = False | |||
if action is None: | |||
action = self._aborting.get(action_id) | |||
was_aborted = action is not None | |||
if action is None: | |||
if is_sdk_action: | |||
logger.error('Received completed action message for unknown SDK action_id=%s', action_id) | |||
return | |||
else: | |||
if not is_sdk_action: | |||
action_id_type = self._action_id_type(action_id) | |||
logger.error('Received completed action message for sdk-known %s action_id=%s (was_aborted=%s)', | |||
action_id_type, action_id, was_aborted) | |||
action._completed_event_pending = True | |||
if was_aborted: | |||
if action._enable_abort_logging: | |||
logger.debug('Received completed action message for aborted action=%s', action) | |||
del self._aborting[action_id] | |||
else: | |||
logger.debug('Received completed action message for in-progress action=%s', action) | |||
del self._in_progress[action_id] | |||
# XXX This should generate a real event, not a msg | |||
# Should also dispatch to self so the parent can be notified. | |||
action.dispatch_event(evt) | |||
def _abort_action(self, action, log_abort_messages): | |||
# Mark this in-progress action as aborting - it should get a "Cancelled" | |||
# message back in the next engine tick, and can basically be considered | |||
# cancelled from now. | |||
action._set_aborting(log_abort_messages) | |||
if action._completed_event_pending: | |||
# The action was marked as still running but the ActionDispatcher | |||
# has already received a completion message (and removed it from | |||
# _in_progress) - the action is just waiting to receive a | |||
# robot_completed_action message that is still being dispatched | |||
# via asyncio.ensure_future | |||
logger.debug('Not sending abort for action=%s to engine as it just completed', action) | |||
else: | |||
# move from in-progress to aborting dicts | |||
self._aborting[action._action_id] = action | |||
del self._in_progress[action._action_id] | |||
msg = _clad_to_engine_iface.CancelActionByIdTag(idTag=action._action_id) | |||
self.robot.conn.send_msg(msg) | |||
def _abort_all_actions(self, log_abort_messages): | |||
# Mark any in-progress actions as aborting - they should get a "Cancelled" | |||
# message back in the next engine tick, and can basically be considered | |||
# cancelled from now. | |||
actions_to_abort = self._in_progress | |||
self._in_progress = {} | |||
for action_id, action in actions_to_abort.items(): | |||
action._set_aborting(log_abort_messages) | |||
self._aborting[action_id] = action | |||
logger.info('Sending abort request for all actions') | |||
# RobotActionType.UNKNOWN is a wildcard that matches all actions when cancelling. | |||
msg = _clad_to_engine_iface.CancelAction(actionType=_clad_to_engine_cozmo.RobotActionType.UNKNOWN) | |||
self.robot.conn.send_msg(msg) | |||
@@ -0,0 +1,223 @@ | |||
# Copyright (c) 2016 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. | |||
''' | |||
Animation related classes, functions, events and values. | |||
''' | |||
# __all__ should order by constants, event classes, other classes, functions. | |||
__all__ = ['EvtAnimationsLoaded', 'EvtAnimationCompleted', | |||
'Animation', 'AnimationTrigger', 'AnimationNames', 'Triggers', | |||
'animation_completed_filter'] | |||
import collections | |||
from . import logger | |||
from . import action | |||
from . import exceptions | |||
from . import event | |||
from ._clad import _clad_to_engine_iface, _clad_to_engine_cozmo | |||
class EvtAnimationsLoaded(event.Event): | |||
'''Triggered when animations names have been received from the engine''' | |||
class EvtAnimationCompleted(action.EvtActionCompleted): | |||
'''Triggered when an animation completes.''' | |||
animation_name = "The name of the animation or trigger that completed" | |||
class Animation(action.Action): | |||
'''An Animation describes an actively-playing animation on a robot.''' | |||
def __init__(self, anim_name, loop_count, ignore_body_track=False, | |||
ignore_head_track=False, ignore_lift_track=False, **kw): | |||
super().__init__(**kw) | |||
#: The name of the animation that was dispatched | |||
self.anim_name = anim_name | |||
#: The number of iterations the animation was requested for | |||
self.loop_count = loop_count | |||
#: bool: True to ignore the body track (i.e. the wheels / treads) | |||
self.ignore_body_track = ignore_body_track | |||
#: bool: True to ignore the head track | |||
self.ignore_head_track = ignore_head_track | |||
#: bool: True to ignore the lift track | |||
self.ignore_lift_track = ignore_lift_track | |||
def _repr_values(self): | |||
all_tracks = {"body":self.ignore_body_track, | |||
"head":self.ignore_head_track, | |||
"lift":self.ignore_lift_track} | |||
ignore_tracks = [k for k, v in all_tracks.items() if v] | |||
return "anim_name=%s loop_count=%s ignore_tracks=%s" % (self.anim_name, self.loop_count, str(ignore_tracks)) | |||
def _encode(self): | |||
return _clad_to_engine_iface.PlayAnimation(animationName=self.anim_name, numLoops=self.loop_count, ignoreBodyTrack=self.ignore_body_track, | |||
ignoreHeadTrack=self.ignore_head_track, ignoreLiftTrack=self.ignore_lift_track) | |||
def _dispatch_completed_event(self, msg): | |||
self._completed_event = EvtAnimationCompleted( | |||
action=self, state=self._state, | |||
animation_name=self.anim_name) | |||
self.dispatch_event(self._completed_event) | |||
class AnimationTrigger(action.Action): | |||
'''An AnimationTrigger represents a playing animation trigger. | |||
Asking Cozmo to play an AnimationTrigger causes him to pick one of the | |||
animations represented by the group. | |||
''' | |||
def __init__(self, trigger, loop_count, use_lift_safe, ignore_body_track, | |||
ignore_head_track, ignore_lift_track, **kw): | |||
super().__init__(**kw) | |||
#: An attribute of :class:`cozmo.anim.Triggers`: The animation trigger dispatched. | |||
self.trigger = trigger | |||
#: int: The number of iterations the animation was requested for | |||
self.loop_count = loop_count | |||
#: bool: True to automatically ignore the lift track if Cozmo is carrying a cube. | |||
self.use_lift_safe = use_lift_safe | |||
#: bool: True to ignore the body track (i.e. the wheels / treads) | |||
self.ignore_body_track = ignore_body_track | |||
#: bool: True to ignore the head track | |||
self.ignore_head_track = ignore_head_track | |||
#: bool: True to ignore the lift track | |||
self.ignore_lift_track = ignore_lift_track | |||
def _repr_values(self): | |||
all_tracks = {"body":self.ignore_body_track, | |||
"head":self.ignore_head_track, | |||
"lift":self.ignore_lift_track} | |||
ignore_tracks = [k for k, v in all_tracks.items() if v] | |||
return "trigger=%s loop_count=%s ignore_tracks=%s use_lift_safe=%s" % ( | |||
self.trigger.name, self.loop_count, str(ignore_tracks), self.use_lift_safe) | |||
def _encode(self): | |||
return _clad_to_engine_iface.PlayAnimationTrigger( | |||
trigger=self.trigger.id, numLoops=self.loop_count, | |||
useLiftSafe=self.use_lift_safe, ignoreBodyTrack=self.ignore_body_track, | |||
ignoreHeadTrack=self.ignore_head_track, ignoreLiftTrack=self.ignore_lift_track) | |||
def _dispatch_completed_event(self, msg): | |||
self._completed_event = EvtAnimationCompleted( | |||
action=self, state=self._state, | |||
animation_name=self.trigger.name) | |||
self.dispatch_event(self._completed_event) | |||
class AnimationNames(event.Dispatcher, set): | |||
'''Holds the set of animation names (strings) returned from the Engine. | |||
Animation names are dynamically retrieved from the engine when the SDK | |||
connects to it, unlike :class:`Triggers` which are defined at runtime. | |||
''' | |||
def __init__(self, conn, **kw): | |||
super().__init__(self, **kw) | |||
self._conn = conn | |||
self._loaded = False | |||
def __contains__(self, key): | |||
if not self._loaded: | |||
raise exceptions.AnimationsNotLoaded("Animations not yet received from engine") | |||
return super().__contains__(key) | |||
def __hash__(self): | |||
# We want to compare AnimationName instances rather than the | |||
# names they contain | |||
return id(self) | |||
def refresh(self): | |||
'''Causes the list of animation names to be re-requested from the engine. | |||
Attempting to play an animation while the list is refreshing will result | |||
in an AnimationsNotLoaded exception being raised. | |||
Generates an EvtAnimationsLoaded event once completed. | |||
''' | |||
self._loaded = False | |||
self.clear() | |||
self._conn.send_msg(_clad_to_engine_iface.RequestAvailableAnimations()) | |||
@property | |||
def is_loaded(self): | |||
'''bool: True if the animation names have been received from the engine.''' | |||
return self._loaded != False | |||
async def wait_for_loaded(self, timeout=None): | |||
'''Wait for the animation names to be loaded from the engine. | |||
Returns: | |||
The :class:`EvtAnimationsLoaded` instance once loaded | |||
Raises: | |||
:class:`asyncio.TimeoutError` | |||
''' | |||
if self._loaded: | |||
return self._loaded | |||
return await self.wait_for(EvtAnimationsLoaded, timeout=timeout) | |||
def _recv_msg_animation_available(self, evt, msg): | |||
name = msg.animName | |||
self.add(name) | |||
def _recv_msg_end_of_message(self, evt, msg): | |||
if not self._loaded: | |||
logger.debug("%d animations loaded", len(self)) | |||
self._loaded = evt | |||
self.dispatch_event(EvtAnimationsLoaded) | |||
# generate names for each CLAD defined trigger | |||
_AnimTrigger = collections.namedtuple('_AnimTrigger', 'name id') | |||
class Triggers: | |||
"""Playing an animation trigger causes the game engine play an animation of a particular type. | |||
The engine may pick one of a number of actual animations to play based on | |||
Cozmo's mood or emotion, or with random weighting. Thus playing the same | |||
trigger twice may not result in the exact same underlying animation playing | |||
twice. | |||
To play an exact animation, use play_anim with a named animation. | |||
This class holds the set of defined animations triggers to pass to play_anim_trigger. | |||
""" | |||
trigger_list = [] | |||
for (_name, _id) in _clad_to_engine_cozmo.AnimationTrigger.__dict__.items(): | |||
if not _name.startswith('_'): | |||
trigger = _AnimTrigger(_name, _id) | |||
setattr(Triggers, _name, trigger) | |||
Triggers.trigger_list.append(trigger) | |||
def animation_completed_filter(): | |||
'''Creates an :class:`cozmo.event.Filter` to wait specifically for an animation completed event.''' | |||
return event.Filter(action.EvtActionCompleted, | |||
action=lambda action: isinstance(action, Animation)) |
@@ -0,0 +1,578 @@ | |||
# 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. | |||
'''Camera image annotation. | |||
.. image:: ../images/annotate.jpg | |||
This module defines an :class:`ImageAnnotator` class used by | |||
:class:`cozmo.world.World` to add annotations to camera images received by Cozmo. | |||
This can include the location of cubes, faces and pets that Cozmo currently sees, | |||
along with user-defined custom annotations. | |||
The ImageAnnotator instance can be accessed as | |||
:attr:`cozmo.world.World.image_annotator`. | |||
''' | |||
# __all__ should order by constants, event classes, other classes, functions. | |||
__all__ = ['DEFAULT_OBJECT_COLORS', | |||
'TOP_LEFT', 'TOP_RIGHT', 'BOTTOM_LEFT', 'BOTTOM_RIGHT', | |||
'RESAMPLE_MODE_NEAREST', 'RESAMPLE_MODE_BILINEAR', | |||
'ImageText', 'Annotator', 'ObjectAnnotator', 'FaceAnnotator', | |||
'PetAnnotator', 'TextAnnotator', 'ImageAnnotator', | |||
'add_img_box_to_image', 'add_polygon_to_image', 'annotator'] | |||
import collections | |||
import functools | |||
try: | |||
from PIL import Image, ImageDraw | |||
except (ImportError, SyntaxError): | |||
# may get SyntaxError if accidentally importing old Python 2 version of PIL | |||
ImageDraw = None | |||
from . import event | |||
from . import objects | |||
DEFAULT_OBJECT_COLORS = { | |||
objects.LightCube: 'yellow', | |||
objects.CustomObject: 'purple', | |||
'default': 'red' | |||
} | |||
LEFT = 1 | |||
RIGHT = 2 | |||
TOP = 4 | |||
BOTTOM = 8 | |||
#: Top left position | |||
TOP_LEFT = TOP | LEFT | |||
#: Bottom left position | |||
BOTTOM_LEFT = BOTTOM | LEFT | |||
#: Top right position | |||
TOP_RIGHT = TOP | RIGHT | |||
#: Bottom right position | |||
BOTTOM_RIGHT = BOTTOM | RIGHT | |||
if ImageDraw is not None: | |||
#: Fastest resampling mode, use nearest pixel | |||
RESAMPLE_MODE_NEAREST = Image.NEAREST | |||
#: Slower, but smoother, resampling mode - linear interpolation from 2x2 grid of pixels | |||
RESAMPLE_MODE_BILINEAR = Image.BILINEAR | |||
else: | |||
RESAMPLE_MODE_NEAREST = None | |||
RESAMPLE_MODE_BILINEAR = None | |||
class ImageText: | |||
'''ImageText represents some text that can be applied to an image. | |||
The class allows the text to be placed at various positions inside a | |||
bounding box within the image itself. | |||
Args: | |||
text (string): The text to display; may contain newlines | |||
position (int): Where on the screen to render the text | |||
- A constant such at TOP_LEFT or BOTTOM_RIGHT | |||
align (string): Text alignment for multi-line strings | |||
color (string): Color to use for the text - see :mod:`PIL.ImageColor` | |||
font (:mod:`PIL.ImageFont`): Font to use (None for a default font) | |||
line_spacing (int): The vertical spacing for multi-line strings | |||
outline_color (string): Color to use for the outline - see | |||
:mod:`PIL.ImageColor` - use None for no outline. | |||
full_outline (bool): True if the outline should surround the text, | |||
otherwise a cheaper drop-shadow is displayed. Only relevant if | |||
outline_color is specified. | |||
''' | |||
def __init__(self, text, position=BOTTOM_RIGHT, align="left", color="white", | |||
font=None, line_spacing=3, outline_color=None, full_outline=True): | |||
self.text = text | |||
self.position = position | |||
self.align = align | |||
self.color = color | |||
self.font = font | |||
self.line_spacing = line_spacing | |||
self.outline_color = outline_color | |||
self.full_outline = full_outline | |||
def render(self, draw, bounds): | |||
'''Renders the text onto an image within the specified bounding box. | |||
Args: | |||
draw (:class:`PIL.ImageDraw.ImageDraw`): The drawable surface to write on | |||
bounds (tuple of int) | |||
(top_left_x, top_left_y, bottom_right_x, bottom_right_y): | |||
bounding box | |||
Returns: | |||
The same :class:`PIL.ImageDraw.ImageDraw` object as was passed-in with text applied. | |||
''' | |||
(bx1, by1, bx2, by2) = bounds | |||
text_width, text_height = draw.textsize(self.text, font=self.font) | |||
if self.position & TOP: | |||
y = by1 | |||
else: | |||
y = by2 - text_height | |||
if self.position & LEFT: | |||
x = bx1 | |||
else: | |||
x = bx2 - text_width | |||
# helper method for each draw call below | |||
def _draw_text(pos, color): | |||
draw.text(pos, self.text, font=self.font, fill=color, | |||
align=self.align, spacing=self.line_spacing) | |||
if self.outline_color is not None: | |||
# Pillow doesn't support outlined or shadowed text directly. | |||
# We manually draw the text multiple times to achieve the effect. | |||
if self.full_outline: | |||
_draw_text((x-1, y), self.outline_color) | |||
_draw_text((x+1, y), self.outline_color) | |||
_draw_text((x, y-1), self.outline_color) | |||
_draw_text((x, y+1), self.outline_color) | |||
else: | |||
# just draw a drop shadow (cheaper) | |||
_draw_text((x+1, y+1), self.outline_color) | |||
_draw_text((x,y), self.color) | |||
return draw | |||
def add_img_box_to_image(image, box, color, text=None): | |||
'''Draw a box on an image and optionally add text. | |||
This will draw the outline of a rectangle to the passed in image | |||
in the specified color and optionally add one or more pieces of text | |||
along the inside edge of the rectangle. | |||
Args: | |||
image (:class:`PIL.Image.Image`): The image to draw on | |||
box (:class:`cozmo.util.ImageBox`): The ImageBox defining the rectangle to draw | |||
color (string): A color string suitable for use with PIL - see :mod:`PIL.ImageColor` | |||
text (instance or iterable of :class:`ImageText`): The text to display | |||
- may be a single ImageText instance, or any iterable (eg a list | |||
of ImageText instances) to display multiple pieces of text. | |||
''' | |||
d = ImageDraw.Draw(image) | |||
x1, y1 = box.left_x, box.top_y | |||
x2, y2 = box.right_x, box.bottom_y | |||
d.rectangle([x1, y1, x2, y2], outline=color) | |||
if text is not None: | |||
if isinstance(text, collections.Iterable): | |||
for t in text: | |||
t.render(d, (x1, y1, x2, y2)) | |||
else: | |||
text.render(d, (x1, y1, x2, y2)) | |||
def add_polygon_to_image(image, poly_points, scale, line_color, fill_color=None): | |||
'''Draw a polygon on an image | |||
This will draw a polygon on the passed-in image in the specified | |||
colors and scale. | |||
Args: | |||
image (:class:`PIL.Image.Image`): The image to draw on | |||
poly_points: A sequence of points representing the polygon, | |||
where each point has float members (x, y) | |||
scale (float): Scale to multiply each point to match the image scaling | |||
line_color (string): The color for the outline of the polygon. The string value | |||
must be a color string suitable for use with PIL - see :mod:`PIL.ImageColor` | |||
fill_color (string): The color for the inside of the polygon. The string value | |||
must be a color string suitable for use with PIL - see :mod:`PIL.ImageColor` | |||
''' | |||
if len(poly_points) < 2: | |||
# Need at least 2 points to draw any lines | |||
return | |||
d = ImageDraw.Draw(image) | |||
# Convert poly_points to the PIL format and scale them to the image | |||
pil_poly_points = [] | |||
for pt in poly_points: | |||
pil_poly_points.append((pt.x * scale, pt.y * scale)) | |||
d.polygon(pil_poly_points, fill=fill_color, outline=line_color) | |||
def _find_key_for_cls(d, cls): | |||
for cls in cls.__mro__: | |||
result = d.get(cls, None) | |||
if result: | |||
return result | |||
return d['default'] | |||
class Annotator: | |||
'''Annotation base class | |||
Subclasses of Annotator handle applying a single annotation to an image. | |||
''' | |||
#: int: The priority of the annotator - Annotators with higher numbered | |||
#: priorities are applied first. | |||
priority = 100 | |||
def __init__(self, img_annotator, priority=None): | |||
#: :class:`ImageAnnotator`: The object managing camera annotations | |||
self.img_annotator = img_annotator | |||
#: :class:`~cozmo.world.World`: The world object for the robot who owns the camera | |||
self.world = img_annotator.world | |||
#: bool: Set enabled to false to prevent the annotator being called | |||
self.enabled = True | |||
if priority is not None: | |||
self.priority = priority | |||
def apply(self, image, scale): | |||
'''Applies the annotation to the image.''' | |||
# should be overriden by a subclass | |||
raise NotImplementedError() | |||
def __hash__(self): | |||
return id(self) | |||
class ObjectAnnotator(Annotator): | |||
'''Adds object annotations to an Image. | |||
This handles :class:`cozmo.objects.LightCube` objects | |||
as well as custom objects. | |||
''' | |||
priority = 100 | |||
object_colors = DEFAULT_OBJECT_COLORS | |||
def __init__(self, img_annotator, object_colors=None): | |||
super().__init__(img_annotator) | |||
if object_colors is not None: | |||
self.object_colors = object_colors | |||
def apply(self, image, scale): | |||
d = ImageDraw.Draw(image) | |||
for obj in self.world.visible_objects: | |||
color = _find_key_for_cls(self.object_colors, obj.__class__) | |||
text = self.label_for_obj(obj) | |||
box = obj.last_observed_image_box | |||
if scale != 1: | |||
box *= scale | |||
add_img_box_to_image(image, box, color, text=text) | |||
def label_for_obj(self, obj): | |||
'''Fetch a label to display for the object. | |||
Override or replace to customize. | |||
''' | |||
return ImageText(obj.descriptive_name) | |||
class FaceAnnotator(Annotator): | |||
'''Adds annotations of currently detected faces to a camera image. | |||
This handles the display of :class:`cozmo.faces.Face` objects. | |||
''' | |||
priority = 100 | |||
box_color = 'green' | |||
def __init__(self, img_annotator, box_color=None): | |||
super().__init__(img_annotator) | |||
if box_color is not None: | |||
self.box_color = box_color | |||
def apply(self, image, scale): | |||
d = ImageDraw.Draw(image) | |||
for obj in self.world.visible_faces: | |||
text = self.label_for_face(obj) | |||
box = obj.last_observed_image_box | |||
if scale != 1: | |||
box *= scale | |||
add_img_box_to_image(image, box, self.box_color, text=text) | |||
add_polygon_to_image(image, obj.left_eye, scale, self.box_color) | |||
add_polygon_to_image(image, obj.right_eye, scale, self.box_color) | |||
add_polygon_to_image(image, obj.nose, scale, self.box_color) | |||
add_polygon_to_image(image, obj.mouth, scale, self.box_color) | |||
def label_for_face(self, obj): | |||
'''Fetch a label to display for the face. | |||
Override or replace to customize. | |||
''' | |||
expression = obj.known_expression | |||
if len(expression) > 0: | |||
# if there is a specific known expression, then also show the score | |||
# (display a % to make it clear the value is out of 100) | |||
expression += "=%s%% " % obj.expression_score | |||
if obj.name: | |||
return ImageText('%s%s (%d)' % (expression, obj.name, obj.face_id)) | |||
return ImageText('(unknown%s face %d)' % (expression, obj.face_id)) | |||
class PetAnnotator(Annotator): | |||
'''Adds annotations of currently detected pets to a camera image. | |||
This handles the display of :class:`cozmo.pets.Pet` objects. | |||
''' | |||
priority = 100 | |||
box_color = 'lightgreen' | |||
def __init__(self, img_annotator, box_color=None): | |||
super().__init__(img_annotator) | |||
if box_color is not None: | |||
self.box_color = box_color | |||
def apply(self, image, scale): | |||
d = ImageDraw.Draw(image) | |||
for obj in self.world.visible_pets: | |||
text = self.label_for_pet(obj) | |||
box = obj.last_observed_image_box | |||
if scale != 1: | |||
box *= scale | |||
add_img_box_to_image(image, box, self.box_color, text=text) | |||
def label_for_pet(self, obj): | |||
'''Fetch a label to display for the pet. | |||
Override or replace to customize. | |||
''' | |||
return ImageText('%d: %s' % (obj.pet_id, obj.pet_type)) | |||
class TextAnnotator(Annotator): | |||
'''Adds simple text annotations to a camera image. | |||
''' | |||
priority = 50 | |||
def __init__(self, img_annotator, text): | |||
super().__init__(img_annotator) | |||
self.text = text | |||
def apply(self, image, scale): | |||
d = ImageDraw.Draw(image) | |||
self.text.render(d, (0, 0, image.width, image.height)) | |||
class _AnnotatorHelper(Annotator): | |||
def __init__(self, img_annotator, wrapped): | |||
super().__init__(img_annotator) | |||
self._wrapped = wrapped | |||
def apply(self, image, scale): | |||
self._wrapped(image, scale, world=self.world, img_annotator=self.img_annotator) | |||
def annotator(f): | |||
'''A decorator for converting a regular function/method into an Annotator. | |||
The wrapped function should have a signature of | |||
``(image, scale, img_annotator=None, world=None, **kw)`` | |||
''' | |||
@functools.wraps(f) | |||
def wrapper(img_annotator): | |||
return _AnnotatorHelper(img_annotator, f) | |||
return wrapper | |||
class ImageAnnotator(event.Dispatcher): | |||
'''ImageAnnotator applies annotations to the camera image received from the robot. | |||
This is instantiated by :class:`cozmo.world.World` and is accessible as | |||
:class:`cozmo.world.World.image_annotator`. | |||
By default it defines three active annotators named ``objects``, ``faces`` and ``pets``. | |||
The ``objects`` annotator adds a box around each object (such as light cubes) | |||
that Cozmo can see. The ``faces`` annotator adds a box around each person's | |||
face that Cozmo can recognize. The ``pets`` annotator adds a box around each pet | |||
face that Cozmo can recognize. | |||
Custom annotations can be defined by calling :meth:`add_annotator` with | |||
a name of your choosing and an instance of a :class:`Annotator` subclass, | |||
or use a regular function wrapped with the :func:`annotator` decorator. | |||
Individual annotations can be disabled and re-enabled using the | |||
:meth:`disable_annotator` and :meth:`enable_annotator` methods. | |||
All annotations can be disabled by setting the | |||
:attr:`annotation_enabled` property to False. | |||
E.g. to disable face annotations, call | |||
``coz.world.image_annotator.disable_annotator('faces')`` | |||
Annotators each have a priority number associated with them. Annotators | |||
with a larger priority number are rendered first and may be overdrawn by those | |||
with a lower/smaller priority number. | |||
''' | |||
def __init__(self, world, **kw): | |||
super().__init__(**kw) | |||
#: :class:`cozmo.world.World`: World object that created the annotator. | |||
self.world = world | |||
self._annotators = {} | |||
self._sorted_annotators = [] | |||
self.add_annotator('objects', ObjectAnnotator(self)) | |||
self.add_annotator('faces', FaceAnnotator(self)) | |||
self.add_annotator('pets', PetAnnotator(self)) | |||
#: If this attribute is set to false, the :meth:`annotate_image` method | |||
#: will continue to provide a scaled image, but will not apply any annotations. | |||
self.annotation_enabled = True | |||
def _sort_annotators(self): | |||
self._sorted_annotators = sorted(self._annotators.values(), | |||
key=lambda an: an.priority, reverse=True) | |||
def add_annotator(self, name, annotator): | |||
'''Adds a new annotator for display. | |||
Annotators are enabled by default. | |||
Args: | |||
name (string): An arbitrary name for the annotator; must not | |||
already be defined | |||
annotator (:class:`Annotator` or callable): The annotator to add | |||
may either by an instance of Annotator, or a factory callable | |||
that will return an instance of Annotator. The callable will | |||
be called with an ImageAnnotator instance as its first argument. | |||
Raises: | |||
:class:`ValueError` if the annotator is already defined. | |||
''' | |||
if name in self._annotators: | |||
raise ValueError('Annotator "%s" is already defined' % (name)) | |||
if not isinstance(annotator, Annotator): | |||
annotator = annotator(self) | |||
self._annotators[name] = annotator | |||
self._sort_annotators() | |||
def remove_annotator(self, name): | |||
'''Remove an annotator. | |||
Args: | |||
name (string): The name of the annotator to remove as passed to | |||
:meth:`add_annotator`. | |||
Raises: | |||
KeyError if the annotator isn't registered | |||
''' | |||
del self._annotators[name] | |||
self._sort_annotators() | |||
def get_annotator(self, name): | |||
'''Return a named annotator. | |||
Args: | |||
name (string): The name of the annotator to return | |||
Raises: | |||
KeyError if the annotator isn't registered | |||
''' | |||
return self._annotators[name] | |||
def disable_annotator(self, name): | |||
'''Disable a named annotator. | |||
Leaves the annotator as registered, but does not include its output | |||
in the annotated image. | |||
Args: | |||
name (string): The name of the annotator to disable | |||
''' | |||
if name in self._annotators: | |||
self._annotators[name].enabled = False | |||
def enable_annotator(self, name): | |||
'''Enabled a named annotator. | |||
(re)enable an annotator if it was previously disabled. | |||
Args: | |||
name (string): The name of the annotator to enable | |||
''' | |||
self._annotators[name].enabled = True | |||
def add_static_text(self, name, text, color='white', position=TOP_LEFT): | |||
'''Add some static text to annotated images. | |||
This is a convenience method to create a :class:`TextAnnnotator` | |||
and add it to the image. | |||
Args: | |||
name (string): An arbitrary name for the annotator; must not | |||
already be defined | |||
text (str or :class:`ImageText` instance): The text to display | |||
may be a plain string, or an ImageText instance | |||
color (string): Used if text is a string; defaults to white | |||
position (int): Used if text is a string; defaults to TOP_LEFT | |||
''' | |||
if isinstance(text, str): | |||
text = ImageText(text, position=position, color=color) | |||
self.add_annotator(name, TextAnnotator(self, text)) | |||
def annotate_image(self, image, scale=None, fit_size=None, resample_mode=RESAMPLE_MODE_NEAREST): | |||
'''Called by :class:`~cozmo.world.World` to annotate camera images. | |||
Args: | |||
image (:class:`PIL.Image.Image`): The image to annotate | |||
scale (float): If set then the base image will be scaled by the | |||
supplied multiplier. Cannot be combined with fit_size | |||
fit_size (tuple of int): If set, then scale the image to fit inside | |||
the supplied (width, height) dimensions. The original aspect | |||
ratio will be preserved. Cannot be combined with scale. | |||
resample_mode (int): The resampling mode to use when scaling the | |||
image. Should be either :attr:`RESAMPLE_MODE_NEAREST` (fast) or | |||
:attr:`RESAMPLE_MODE_BILINEAR` (slower, but smoother). | |||
Returns: | |||
:class:`PIL.Image.Image` | |||
''' | |||
if ImageDraw is None: | |||
return image | |||
if scale is not None: | |||
if scale == 1: | |||
image = image.copy() | |||
else: | |||
image = image.resize((int(image.width * scale), int(image.height * scale)), | |||
resample=resample_mode) | |||
elif fit_size is not None: | |||
if fit_size == (image.width, image.height): | |||
image = image.copy() | |||
scale = 1 | |||
else: | |||
img_ratio = image.width / image.height | |||
fit_width, fit_height = fit_size | |||
fit_ratio = fit_width / fit_height | |||
if img_ratio > fit_ratio: | |||
fit_height = int(fit_width / img_ratio) | |||
elif img_ratio < fit_ratio: | |||
fit_width = int(fit_height * img_ratio) | |||
scale = fit_width / image.width | |||
image = image.resize((fit_width, fit_height)) | |||
else: | |||
scale = 1 | |||
if not self.annotation_enabled: | |||
return image | |||
for an in self._sorted_annotators: | |||
if an.enabled: | |||
an.apply(image, scale) | |||
return image |
@@ -0,0 +1,21 @@ | |||
Anki, Inc. Image and 3D Model License Agreement Version 1.0 (last updated March 28, 2017) | |||
This Image and 3D Model License Agreement (this "Agreement") governs the terms and conditions of your access to and use of Licensed Materials (as defined below), and is made between you, as an individual or entity ("you"), and Anki, Inc. ("we," "us" or "Licensor"). You accept and agree to be bound by this Agreement by your access to or use of any of the Licensed Materials. | |||
The "Licensed Materials" are the digital images, and 3D models, that we make available to you from time to time in connection with this Agreement. | |||
1. License. Subject to the terms and conditions of this Agreement, we hereby grant you a limited, revocable, worldwide, fully-paid, royalty free, non-exclusive, non-transferable copyright license during the term of this Agreement to access, copy, display, perform, modify the size of, and distribute, in any of the Licensed Materials, in each case: (A) solely in connection with your use of the Cozmo SDK in accordance with our separate SDK license agreement(s) or the applicable Anki hardware products (e.g. Cozmo) and/or the Cozmo App, and (B) only provided that you comply with the Anki Terms of Use at www.anki.com/terms and any other terms that may apply to the Cozmo device and/or Cozmo mobile application and that we may from time to time modify. Licensee may not sublicense any of the foregoing rights, except for the right to access, copy, display perform and distribute the Licensed Materials only in connection with the SDK in an app created by the Licensee. For clarity, this license does not include the right to commercially distribute the Licensed Materials in print form. | |||
2. Reservation. Licensor (or its suppliers) owns and retains all right, title, and interest in and to each of the Licensed Materials worldwide including, but not limited to, ownership of all copyrights and other intellectual property rights therein. We reserve all rights not explicitly licensed in this Agreement. | |||
3. DISCLAIMER OF WARRANTY AND LIMITATION OF LIABILITY. THE LICENSED MATERIALS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NONINFRINGEMENT. IN NO EVENT WILL LICENSOR BE LIABLE TO YOU OR TO ANY THIRD PARTY FOR ANY DIRECT, INDIRECT, SPECIAL, INCIDENTAL, PUNITIVE OR CONSEQUENTIAL DAMAGES, OR LOST REVENUE, SAVINGS OR PROFITS, WHETHER BASED ON BREACH OF CONTRACT, TORT (INCLUDING NEGLIGENCE) OR OTHERWISE, ARISING FROM OR IN CONNECTION WITH ANY OF THE LICENSED MATERIALS, WHETHER OR NOT THAT PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |||
4. Indemnification. You will defend, indemnify and hold harmless Licensor and its officers, directors, shareholders, employees, and agents from any loss, liability, cost or expense, including attorneys' fees ("Liabilities") that arises from any claim, action or proceeding in connection with your use of the Licensed Materials or your breach of this Agreement. You shall have control of the defense and all related settlement negotiations for such claim, action or proceeding; provided that we shall have the right to consent to any settlement or entry of judgment, such consent not to be unreasonably withheld, and we may participate in such defense using our own counsel at our own expense. | |||
5. Termination. You may terminate this Agreement at any time by deleting or destroying all copies of the Licensed Materials that you possess or control. We may terminate this Agreement and/or your license to any or all of the Licensed Materials at any time without prior notice to you. In case of termination, you must cease all access and use of, and delete or destroy, all copies of the Licensed Materials that you possess or control. Sections 2 through 8 of this Agreement will survive termination of this Agreement. | |||
6. Modifications to this Agreement and Licensed Materials. We may amend this Agreement at any time by posting an amended version online and/or sending information regarding the amendment to your email address of record with us. You shall be deemed to have accepted such amendments by continuing to access and/or use any Licensed Materials after such amendments have been posted or information regarding such amendments has been sent to you. If you do not agree to any of such changes, you may terminate this Agreement and immediately cease all access to and use of Licensed Materials. You agree that such termination will be your exclusive remedy in such event. No other waiver or modification of this Agreement shall be valid unless in writing and signed by both parties. We also reserve the right at any time and from time to time to modify or discontinue all or any portion of any Licensed Materials without notice to you. We shall not be liable to you or any third party should we exercise such rights. | |||
7. Assignment. You may not assign this Agreement, in whole or in part, without our prior written consent, and any attempt by you to assign this Agreement without such consent shall be void. Subject to the foregoing, this Agreement shall benefit and bind both parties, and their successors and permitted assigns. | |||
8. General. You shall comply with all laws, rules and regulations applicable to your activities under this Agreement. This Agreement shall be governed by and construed in accordance with the laws of the State of California, U.S.A., except for its conflicts of laws principles. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. This Agreement will not be construed to create or imply any partnership, agency or joint venture between the parties. If any provision of this Agreement is found illegal or unenforceable, it will be enforced to the maximum extent permissible, and the legality and enforceability of the other provisions of this Agreement will not be affected. This Agreement is the complete agreement between the parties with respect to its subject matter, and supersedes any prior agreements and communications (both written and oral) regarding such subject matter. |
@@ -0,0 +1,67 @@ | |||
# rim around the screen | |||
newmtl ScreenOp_matSG | |||
illum 4 | |||
Kd 0.00 0.00 0.00 0.2 | |||
Ka 0.00 0.00 0.00 0.2 | |||
Tf 1.00 1.00 1.00 | |||
Ni 1.00 | |||
Ks 0.08 0.08 0.08 0.2 | |||
Ns 10 | |||
# glass screen | |||
newmtl anisotropic1SG | |||
illum 4 | |||
Kd 0.00 0.00 0.00 0.2 | |||
Ka 0.00 0.00 0.00 0.2 | |||
Tf 0.09 0.09 0.09 | |||
Ni 1.00 | |||
Ks 0.1 0.1 0.1 0.2 | |||
Ns 10 | |||
# head, arms+fork, wheels (but not treads) | |||
newmtl blinn2SG | |||
illum 4 | |||
Kd 0.82 0.82 0.82 | |||
Ka 0.00 0.00 0.00 | |||
Tf 1.00 1.00 1.00 | |||
Ni 1.00 | |||
Ks 0.50 0.50 0.50 | |||
Ns 10 | |||
# Treads | |||
newmtl blinn3SG | |||
illum 4 | |||
Kd 0.10 0.10 0.10 | |||
Ka 0.00 0.00 0.00 | |||
Tf 1.00 1.00 1.00 | |||
Ni 1.00 | |||
Ks 0.5 0.5 0.5 | |||
Ns 10 | |||
# Body and wheel-spacers | |||
newmtl blinn4SG | |||
illum 4 | |||
Kd 0.70 0.70 0.70 | |||
Ka 0.00 0.00 0.00 | |||
Tf 1.00 1.00 1.00 | |||
Ni 1.00 | |||
Ks 0.50 0.50 0.50 | |||
Ns 10 | |||
# Eyelids | |||
newmtl lambert2SG | |||
illum 4 | |||
Kd 0.00 0.00 0.00 | |||
Ka 0.00 0.00 0.00 | |||
Tf 1.00 1.00 1.00 | |||
Ni 1.00 | |||
Ks 0.50 0.50 0.50 | |||
Ns 10 | |||
# eyes | |||
newmtl shadingMap1SG | |||
illum 4 | |||
Kd 0.00 1.00 1.00 | |||
Ka 0.00 1.00 1.00 | |||
Tf 1.00 1.00 1.00 | |||
Ni 0.00 |
@@ -0,0 +1,9 @@ | |||
newmtl cube_mtl | |||
map_Kd cube1.jpg | |||
illum 4 | |||
Kd 0.65 0.65 0.65 | |||
Ka 0.15 0.15 0.15 | |||
Tf 1.00 1.00 1.00 | |||
Ni 1.00 | |||
Ks 0.99 0.99 0.99 | |||
Ns 10 |
@@ -0,0 +1,9 @@ | |||
newmtl cube_mtl | |||
map_Kd cube2.jpg | |||
illum 4 | |||
Kd 0.65 0.65 0.65 | |||
Ka 0.15 0.15 0.15 | |||
Tf 1.00 1.00 1.00 | |||
Ni 1.00 | |||
Ks 0.99 0.99 0.99 | |||
Ns 10 |
@@ -0,0 +1,9 @@ | |||
newmtl cube_mtl | |||
map_Kd cube3.jpg | |||
illum 4 | |||
Kd 0.65 0.65 0.65 | |||
Ka 0.15 0.15 0.15 | |||
Tf 1.00 1.00 1.00 | |||
Ni 1.00 | |||
Ks 0.99 0.99 0.99 | |||
Ns 10 |
@@ -0,0 +1,379 @@ | |||
# Copyright (c) 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. | |||
''' | |||
Audio related classes, functions, events and values. | |||
''' | |||
# __all__ should order by constants, event classes, other classes, functions. | |||
__all__ = ['AudioEvents'] | |||
import collections | |||
from . import logger | |||
from . import action | |||
from . import exceptions | |||
from . import event | |||
from ._clad import _clad_to_engine_iface, _clad_to_engine_cozmo, _clad_to_engine_anki, CladEnumWrapper | |||
# generate names for each CLAD defined trigger | |||
class _AudioEvent(collections.namedtuple('_AudioEvent', 'name id')): | |||
# Tuple mapping between CLAD AudioEvents name and ID | |||
# All instances will be members of AudioEvents | |||
# Keep _AudioEvent as lightweight as a normal namedtuple | |||
__slots__ = () | |||
def __str__(self): | |||
return 'AudioEvents.%s' % self.name | |||
class AudioEvents(CladEnumWrapper): | |||
"""The possible values for an AudioEvent. | |||
Pass one of these event objects to robot.play_audio() to play the corresponding sound clip. | |||
Example: ``robot.play_audio(cozmo.audio.AudioEvents.MusicFunLoop)`` | |||
""" | |||
_clad_enum = _clad_to_engine_anki.AudioMetaData.GameEvent.Codelab | |||
_entry_type = _AudioEvent | |||
#: Reserved Id for invalid sound events | |||
Invalid = _entry_type("Invalid", _clad_enum.Invalid) | |||
#: Stop all playing music | |||
MusicGlobalStop = _entry_type("MusicGlobalStop", _clad_enum.Music_Global_Stop) | |||
#: Mute cozmo background music | |||
MusicBackgroundSilenceOn = _entry_type("MusicBackgroundSilenceOn", _clad_enum.Music_Background_Silence_On) | |||
#: Unmute cozmo background music | |||
MusicBackgroundSilenceOff = _entry_type("MusicBackgroundSilenceOff", _clad_enum.Music_Background_Silence_Off) | |||
#: Initialize the synchronized tiny orchestra system | |||
#: (Will not produce any sound on its own, one of the modes must be triggered) | |||
MusicTinyOrchestraInit = _entry_type("MusicTinyOrchestraInit", _clad_enum.Music_Tiny_Orchestra_Init) | |||
#: Turn off the synchronized tiny orchestra system | |||
MusicTinyOrchestraStop = _entry_type("MusicTinyOrchestraStop", _clad_enum.Music_Tiny_Orchestra_Stop) | |||
#: Turn on the first mode of the synchronized tiny orchestra bass channel | |||
#: (Requires the tiny orchestra system be initialized, and will loop until the system is turned off) | |||
MusicTinyOrchestraBassMode1 = _entry_type("MusicTinyOrchestraBassMode1", _clad_enum.Music_Tiny_Orchestra_Bass_Mode_1) | |||
#: Turn off the first mode of the synchronized tiny orchestra bass channel | |||
MusicTinyOrchestraBassMode1Stop = _entry_type("MusicTinyOrchestraBassMode1Stop", _clad_enum.Music_Tiny_Orchestra_Bass_Mode_1_Stop) | |||
#: Turn on the second mode of the synchronized tiny orchestra bass channel | |||
#: (Requires the tiny orchestra system be initialized, and will loop until the system is turned off) | |||
MusicTinyOrchestraBassMode2 = _entry_type("MusicTinyOrchestraBassMode2", _clad_enum.Music_Tiny_Orchestra_Bass_Mode_2) | |||
#: Turn off the second mode of the synchronized tiny orchestra bass channel | |||
MusicTinyOrchestraBassMode2Stop = _entry_type("MusicTinyOrchestraBassMode2Stop", _clad_enum.Music_Tiny_Orchestra_Bass_Mode_2_Stop) | |||
#: Turn on the third mode of the synchronized tiny orchestra bass channel | |||
#: (Requires the tiny orchestra system be initialized, and will loop until the system is turned off) | |||
MusicTinyOrchestraBassMode3 = _entry_type("MusicTinyOrchestraBassMode3", _clad_enum.Music_Tiny_Orchestra_Bass_Mode_3) | |||
#: Turn off the third mode of the synchronized tiny orchestra bass channel | |||
MusicTinyOrchestraBassMode3Stop = _entry_type("MusicTinyOrchestraBassMode3Stop", _clad_enum.Music_Tiny_Orchestra_Bass_Mode_3_Stop) | |||
#: Turn off all synchronized tiny orchestra bass channels | |||
MusicTinyOrchestraBassStop = _entry_type("MusicTinyOrchestraBassStop", _clad_enum.Music_Tiny_Orchestra_Bass_Stop) | |||
#: Turn on the first mode of the synchronized tiny orchestra glock pluck channel | |||
#: (Requires the tiny orchestra system be initialized, and will loop until the system is turned off) | |||
MusicTinyOrchestraGlockPluckMode1 = _entry_type("MusicTinyOrchestraGlockPluckMode1", _clad_enum.Music_Tiny_Orchestra_Glock_Pluck_Mode_1) | |||
#: Turn off the first mode of the synchronized tiny orchestra glock pluck channel | |||
MusicTinyOrchestraGlockPluckMode1Stop = _entry_type("MusicTinyOrchestraGlockPluckMode1Stop", _clad_enum.Music_Tiny_Orchestra_Glock_Pluck_Mode_1_Stop) | |||
#: Turn on the second mode of the synchronized tiny orchestra glock pluck channel | |||
#: (Requires the tiny orchestra system be initialized, and will loop until the system is turned off) | |||
MusicTinyOrchestraGlockPluckMode2 = _entry_type("MusicTinyOrchestraGlockPluckMode2", _clad_enum.Music_Tiny_Orchestra_Glock_Pluck_Mode_2) | |||
#: Turn off the second mode of the synchronized tiny orchestra glock pluck channel | |||
MusicTinyOrchestraGlockPluckMode2Stop = _entry_type("MusicTinyOrchestraGlockPluckMode2Stop", _clad_enum.Music_Tiny_Orchestra_Glock_Pluck_Mode_2_Stop) | |||
#: Turn on the third mode of the synchronized tiny orchestra glock pluck channel | |||
#: (Requires the tiny orchestra system be initialized, and will loop until the system is turned off) | |||
MusicTinyOrchestraGlockPluckMode3 = _entry_type("MusicTinyOrchestraGlockPluckMode3", _clad_enum.Music_Tiny_Orchestra_Glock_Pluck_Mode_3) | |||
#: Turn off the third mode of the synchronized tiny orchestra glock pluck channel | |||
MusicTinyOrchestraGlockPluckMode3Stop = _entry_type("MusicTinyOrchestraGlockPluckMode3Stop", _clad_enum.Music_Tiny_Orchestra_Glock_Pluck_Mode_3_Stop) | |||
#: Turn off all synchronized tiny orchestra glock pluck channels | |||
MusicTinyOrchestraGlockPluckStop = _entry_type("MusicTinyOrchestraGlockPluckStop", _clad_enum.Music_Tiny_Orchestra_Glock_Pluck_Stop) | |||
#: Turn on the first mode of the synchronized tiny orchestra strings channel | |||
#: (Requires the tiny orchestra system be initialized, and will loop until the system is turned off) | |||
MusicTinyOrchestraStringsMode1 = _entry_type("MusicTinyOrchestraStringsMode1", _clad_enum.Music_Tiny_Orchestra_Strings_Mode_1) | |||
#: Turn off the first mode of the synchronized tiny orchestra strings channel | |||
MusicTinyOrchestraStringsMode1Stop = _entry_type("MusicTinyOrchestraStringsMode1Stop", _clad_enum.Music_Tiny_Orchestra_Strings_Mode_1_Stop) | |||
#: Turn on the second mode of the synchronized tiny orchestra strings channel | |||
#: (Requires the tiny orchestra system be initialized, and will loop until the system is turned off) | |||
MusicTinyOrchestraStringsMode2 = _entry_type("MusicTinyOrchestraStringsMode2", _clad_enum.Music_Tiny_Orchestra_Strings_Mode_2) | |||
#: Turn off the second mode of the synchronized tiny orchestra strings channel | |||
MusicTinyOrchestraStringsMode2Stop = _entry_type("MusicTinyOrchestraStringsMode2Stop", _clad_enum.Music_Tiny_Orchestra_Strings_Mode_2_Stop) | |||
#: Turn on the third mode of the synchronized tiny orchestra strings channel | |||
#: (Requires the tiny orchestra system be initialized, and will loop until the system is turned off) | |||
MusicTinyOrchestraStringsMode3 = _entry_type("MusicTinyOrchestraStringsMode3", _clad_enum.Music_Tiny_Orchestra_Strings_Mode_3) | |||
#: Turn off the third mode of the synchronized tiny orchestra strings channel | |||
MusicTinyOrchestraStringsMode3Stop = _entry_type("MusicTinyOrchestraStringsMode3Stop", _clad_enum.Music_Tiny_Orchestra_Strings_Mode_3_Stop) | |||
#: Turn off all synchronized tiny orchestra strings channels | |||
MusicTinyOrchestraStringsStop = _entry_type("MusicTinyOrchestraStringsStop", _clad_enum.Music_Tiny_Orchestra_Strings_Stop) | |||
#: Plays the first tiny orchestra bass track | |||
#: (Does not repeat. Does not interact with the synchronized tiny orchestra system) | |||
MusicTinyOrchestraBass01Loop = _entry_type("MusicTinyOrchestraBass01Loop", _clad_enum.Music_Tiny_Orchestra_Bass_01_Loop) | |||
#: Stops active plays of the first tiny orchestra bass track | |||
MusicTinyOrchestraBass01LoopStop = _entry_type("MusicTinyOrchestraBass01LoopStop", _clad_enum.Music_Tiny_Orchestra_Bass_01_Loop_Stop) | |||
#: Plays the second tiny orchestra bass track | |||
#: (Does not repeat. Does not interact with the synchronized tiny orchestra system) | |||
MusicTinyOrchestraBass02Loop = _entry_type("MusicTinyOrchestraBass02Loop", _clad_enum.Music_Tiny_Orchestra_Bass_02_Loop) | |||
#: Stops active plays of the second tiny orchestra bass track | |||
MusicTinyOrchestraBass02LoopStop = _entry_type("MusicTinyOrchestraBass02LoopStop", _clad_enum.Music_Tiny_Orchestra_Bass_02_Loop_Stop) | |||
#: Plays the third tiny orchestra bass track | |||
#: (Does not repeat. Does not interact with the synchronized tiny orchestra system) | |||
MusicTinyOrchestraBass03Loop = _entry_type("MusicTinyOrchestraBass03Loop", _clad_enum.Music_Tiny_Orchestra_Bass_03_Loop) | |||
#: Stops active plays of the third tiny orchestra bass track | |||
MusicTinyOrchestraBass03LoopStop = _entry_type("MusicTinyOrchestraBass03LoopStop", _clad_enum.Music_Tiny_Orchestra_Bass_03_Loop_Stop) | |||
#: Plays the first tiny orchestra glock pluck track | |||
#: (Does not repeat. Does not interact with the synchronized tiny orchestra system) | |||
MusicTinyOrchestraGlockPluck01Loop = _entry_type("MusicTinyOrchestraGlockPluck01Loop", _clad_enum.Music_Tiny_Orchestra_Glock_Pluck_01_Loop) | |||
#: Stops active plays of the first tiny orchestra glock pluck track | |||
MusicTinyOrchestraGlockPluck01LoopStop = _entry_type("MusicTinyOrchestraGlockPluck01LoopStop", _clad_enum.Music_Tiny_Orchestra_Glock_Pluck_01_Loop_Stop) | |||
#: Plays the second tiny orchestra glock pluck track | |||
#: (Does not repeat. Does not interact with the synchronized tiny orchestra system) | |||
MusicTinyOrchestraGlockPluck02Loop = _entry_type("MusicTinyOrchestraGlockPluck02Loop", _clad_enum.Music_Tiny_Orchestra_Glock_Pluck_02_Loop) | |||
#: Stops active plays of the second tiny orchestra glock pluck track | |||
MusicTinyOrchestraGlockPluck02LoopStop = _entry_type("MusicTinyOrchestraGlockPluck02LoopStop", _clad_enum.Music_Tiny_Orchestra_Glock_Pluck_02_Loop_Stop) | |||
#: Plays the third tiny orchestra glock pluck track | |||
#: (Does not repeat. Does not interact with the synchronized tiny orchestra system) | |||
MusicTinyOrchestraGlockPluck03Loop = _entry_type("MusicTinyOrchestraGlockPluck03Loop", _clad_enum.Music_Tiny_Orchestra_Glock_Pluck_03_Loop) | |||
#: Stops active plays of the third tiny orchestra glock pluck track | |||
MusicTinyOrchestraGlockPluck03LoopStop = _entry_type("MusicTinyOrchestraGlockPluck03LoopStop", _clad_enum.Music_Tiny_Orchestra_Glock_Pluck_03_Loop_Stop) | |||
#: Plays the first tiny orchestra string track | |||
#: (Does not repeat. Does not interact with the synchronized tiny orchestra system) | |||
MusicTinyOrchestraStrings01Loop = _entry_type("MusicTinyOrchestraStrings01Loop", _clad_enum.Music_Tiny_Orchestra_Strings_01_Loop) | |||
#: Stops active plays of the first tiny orchestra strings track | |||
MusicTinyOrchestraStrings01LoopStop = _entry_type("MusicTinyOrchestraStrings01LoopStop", _clad_enum.Music_Tiny_Orchestra_Strings_01_Loop_Stop) | |||
#: Plays the second tiny orchestra string track | |||
#: (Does not repeat. Does not interact with the synchronized tiny orchestra system) | |||
MusicTinyOrchestraStrings02Loop = _entry_type("MusicTinyOrchestraStrings02Loop", _clad_enum.Music_Tiny_Orchestra_Strings_02_Loop) | |||
#: Stops active plays of the second tiny orchestra strings track | |||
MusicTinyOrchestraStrings02LoopStop = _entry_type("MusicTinyOrchestraStrings02LoopStop", _clad_enum.Music_Tiny_Orchestra_Strings_02_Loop_Stop) | |||
#: Plays the third tiny orchestra string track | |||
#: (Does not repeat. Does not interact with the synchronized tiny orchestra system) | |||
MusicTinyOrchestraStrings03Loop = _entry_type("MusicTinyOrchestraStrings03Loop", _clad_enum.Music_Tiny_Orchestra_Strings_03_Loop) | |||
#: Stops active plays of the third tiny orchestra strings track | |||
MusicTinyOrchestraStrings03LoopStop = _entry_type("MusicTinyOrchestraStrings03LoopStop", _clad_enum.Music_Tiny_Orchestra_Strings_03_Loop_Stop) | |||
#: Plays the cube whack music | |||
MusicCubeWhack = _entry_type("MusicCubeWhack", _clad_enum.Music_Cube_Whack) | |||
#: Plays the level 1 hot potato music | |||
#: (Does not repeat) | |||
MusicHotPotatoLevel1Loop = _entry_type("MusicHotPotatoLevel1Loop", _clad_enum.Music_Hot_Potato_Level_1_Loop) | |||
#: Stops active plays of the level 1 hot potato music | |||
MusicHotPotatoLevel1LoopStop = _entry_type("MusicHotPotatoLevel1LoopStop", _clad_enum.Music_Hot_Potato_Level_1_Loop_Stop) | |||
#: Plays the level 2 hot potato music | |||
#: (Does not repeat) | |||
MusicHotPotatoLevel2Loop = _entry_type("MusicHotPotatoLevel2Loop", _clad_enum.Music_Hot_Potato_Level_2_Loop) | |||
#: Stops active plays of the level 2 hot potato music | |||
MusicHotPotatoLevel2LoopStop = _entry_type("MusicHotPotatoLevel2LoopStop", _clad_enum.Music_Hot_Potato_Level_2_Loop_Stop) | |||
#: Plays the level 3 hot potato music | |||
#: (Does not repeat) | |||
MusicHotPotatoLevel3Loop = _entry_type("MusicHotPotatoLevel3Loop", _clad_enum.Music_Hot_Potato_Level_3_Loop) | |||
#: Stops active plays of the level 3 hot potato music | |||
MusicHotPotatoLevel3LoopStop = _entry_type("MusicHotPotatoLevel3LoopStop", _clad_enum.Music_Hot_Potato_Level_3_Loop_Stop) | |||
#: Plays the level 4 hot potato music | |||
#: (Does not repeat) | |||
MusicHotPotatoLevel4Loop = _entry_type("MusicHotPotatoLevel4Loop", _clad_enum.Music_Hot_Potato_Level_4_Loop) | |||
#: Stops active plays of the level 4 hot potato music | |||
MusicHotPotatoLevel4LoopStop = _entry_type("MusicHotPotatoLevel4LoopStop", _clad_enum.Music_Hot_Potato_Level_4_Loop_Stop) | |||
#: Plays the magic fortune teller reveal music | |||
MusicMagic8RevealStinger = _entry_type("MusicMagic8RevealStinger", _clad_enum.Music_Magic8_Reveal_Stinger) | |||
#: Stops active plays of the magic fortune teller reveal music | |||
MusicMagic8RevealStingerStop = _entry_type("MusicMagic8RevealStingerStop", _clad_enum.Music_Magic8_Reveal_Stinger_Stop) | |||
#: Plays 80s style music | |||
#: (Does not repeat) | |||
MusicStyle80S1159BpmLoop = _entry_type("MusicStyle80S1159BpmLoop", _clad_enum.Music_Style_80S_1_159Bpm_Loop) | |||
#: Stops active plays of 80s style music | |||
MusicStyle80S1159BpmLoopStop = _entry_type("MusicStyle80S1159BpmLoopStop", _clad_enum.Music_Style_80S_1_159Bpm_Loop_Stop) | |||
#: Plays disco style music | |||
#: (Does not repeat) | |||
MusicStyleDisco1135BpmLoop = _entry_type("MusicStyleDisco1135BpmLoop", _clad_enum.Music_Style_Disco_1_135Bpm_Loop) | |||
#: Stops active plays of disco style music | |||
MusicStyleDisco1135BpmLoopStop = _entry_type("MusicStyleDisco1135BpmLoopStop", _clad_enum.Music_Style_Disco_1_135Bpm_Loop_Stop) | |||
#: Plays mambo style music | |||
#: (Does not repeat) | |||
MusicStyleMambo1183BpmLoop = _entry_type("MusicStyleMambo1183BpmLoop", _clad_enum.Music_Style_Mambo_1_183Bpm_Loop) | |||
#: Stops active plays of mambo style music | |||
MusicStyleMambo1183BpmLoopStop = _entry_type("MusicStyleMambo1183BpmLoopStop", _clad_enum.Music_Style_Mambo_1_183Bpm_Loop_Stop) | |||
#: Stops all playing sound effects | |||
SfxGlobalStop = _entry_type("SfxGlobalStop", _clad_enum.Sfx_Global_Stop) | |||
#: Plays cube light sound | |||
SfxCubeLight = _entry_type("SfxCubeLight", _clad_enum.Sfx_Cube_Light) | |||
#: Stops active plays of cube light sound | |||
SfxCubeLightStop = _entry_type("SfxCubeLightStop", _clad_enum.Sfx_Cube_Light_Stop) | |||
#: Plays firetruck timer start sound | |||
SfxFiretruckTimerStart = _entry_type("SfxFiretruckTimerStart", _clad_enum.Sfx_Firetruck_Timer_Start) | |||
#: Stops active plays of firetruck timer start sound | |||
SfxFiretruckTimerStartStop = _entry_type("SfxFiretruckTimerStartStop", _clad_enum.Sfx_Firetruck_Timer_Start_Stop) | |||
#: Plays firetruck timer end sound | |||
SfxFiretruckTimerEnd = _entry_type("SfxFiretruckTimerEnd", _clad_enum.Sfx_Firetruck_Timer_End) | |||
#: Stops active plays of firetruck timer end sound | |||
SfxFiretruckTimerEndStop = _entry_type("SfxFiretruckTimerEndStop", _clad_enum.Sfx_Firetruck_Timer_End_Stop) | |||
#: Plays game win sound | |||
SfxGameWin = _entry_type("SfxGameWin", _clad_enum.Sfx_Game_Win) | |||
#: Stops active plays of game win sound | |||
SfxGameWinStop = _entry_type("SfxGameWinStop", _clad_enum.Sfx_Game_Win_Stop) | |||
#: Plays game lose sound | |||
SfxGameLose = _entry_type("SfxGameLose", _clad_enum.Sfx_Game_Lose) | |||
#: Stops active plays of game lose sound | |||
SfxGameLoseStop = _entry_type("SfxGameLoseStop", _clad_enum.Sfx_Game_Lose_Stop) | |||
#: Plays hot potato cube charge sound | |||
SfxHotPotatoCubeCharge = _entry_type("SfxHotPotatoCubeCharge", _clad_enum.Sfx_Hot_Potato_Cube_Charge) | |||
#: Stops active plays of hot potato cube charge sound | |||
SfxHotPotatoCubeChargeStop = _entry_type("SfxHotPotatoCubeChargeStop", _clad_enum.Sfx_Hot_Potato_Cube_Charge_Stop) | |||
#: Plays hot potato cube ready sound | |||
SfxHotPotatoCubeReady = _entry_type("SfxHotPotatoCubeReady", _clad_enum.Sfx_Hot_Potato_Cube_Ready) | |||
#: Stops active plays of hot potato cube ready sound | |||
SfxHotPotatoCubeReadyStop = _entry_type("SfxHotPotatoCubeReadyStop", _clad_enum.Sfx_Hot_Potato_Cube_Ready_Stop) | |||
#: Plays hot potato pass sound | |||
SfxHotPotatoPass = _entry_type("SfxHotPotatoPass", _clad_enum.Sfx_Hot_Potato_Pass) | |||
#: Stops active plays of hot potato pass sound | |||
SfxHotPotatoPassStop = _entry_type("SfxHotPotatoPassStop", _clad_enum.Sfx_Hot_Potato_Pass_Stop) | |||
#: Plays hot potato timer end sound | |||
SfxHotPotatoTimerEnd = _entry_type("SfxHotPotatoTimerEnd", _clad_enum.Sfx_Hot_Potato_Timer_End) | |||
#: Stops active plays of hot potato timer end sound | |||
SfxHotPotatoTimerEndStop = _entry_type("SfxHotPotatoTimerEndStop", _clad_enum.Sfx_Hot_Potato_Timer_End_Stop) | |||
#: Plays magic fortune teller message reveal sound | |||
SfxMagic8MessageReveal = _entry_type("SfxMagic8MessageReveal", _clad_enum.Sfx_Magic8_Message_Reveal) | |||
#: Stops active plays of magic fortune teller message reveal sound | |||
SfxMagic8MessageRevealStop = _entry_type("SfxMagic8MessageRevealStop", _clad_enum.Sfx_Magic8_Message_Reveal_Stop) | |||
#: Plays magnet attract sound | |||
SfxMagnetAttract = _entry_type("SfxMagnetAttract", _clad_enum.Sfx_Magnet_Attract) | |||
#: Stops active plays of magnet attrack sound | |||
SfxMagnetAttractStop = _entry_type("SfxMagnetAttractStop", _clad_enum.Sfx_Magnet_Attract_Stop) | |||
#: Plays magnet repel sound | |||
SfxMagnetRepel = _entry_type("SfxMagnetRepel", _clad_enum.Sfx_Magnet_Repel) | |||
#: Stops active plays of magnet repel sound | |||
SfxMagnetRepelStop = _entry_type("SfxMagnetRepelStop", _clad_enum.Sfx_Magnet_Repel_Stop) | |||
#: Plays countdown sound | |||
SfxSharedCountdown = _entry_type("SfxSharedCountdown", _clad_enum.Sfx_Shared_Countdown) | |||
#: Stops active plays of countdown sound | |||
SfxSharedCountdownStop = _entry_type("SfxSharedCountdownStop", _clad_enum.Sfx_Shared_Countdown_Stop) | |||
#: Plays cube light on sound | |||
SfxSharedCubeLightOn = _entry_type("SfxSharedCubeLightOn", _clad_enum.Sfx_Shared_Cube_Light_On) | |||
#: Stops active plays of cube light on sound | |||
SfxSharedCubeLightOnStop = _entry_type("SfxSharedCubeLightOnStop", _clad_enum.Sfx_Shared_Cube_Light_On_Stop) | |||
#: Plays error sound | |||
SfxSharedError = _entry_type("SfxSharedError", _clad_enum.Sfx_Shared_Error) | |||
#: Stops active plays of error sound | |||
SfxSharedErrorStop = _entry_type("SfxSharedErrorStop", _clad_enum.Sfx_Shared_Error_Stop) | |||
#: Plays success sound | |||
SfxSharedSuccess = _entry_type("SfxSharedSuccess", _clad_enum.Sfx_Shared_Success) | |||
#: Stops active plays of success sound | |||
SfxSharedSuccessStop = _entry_type("SfxSharedSuccessStop", _clad_enum.Sfx_Shared_Success_Stop) | |||
#: Plays timer click sound | |||
SfxSharedTimerClick = _entry_type("SfxSharedTimerClick", _clad_enum.Sfx_Shared_Timer_Click) | |||
#: Stops active plays of timer click sound | |||
SfxSharedTimerClickStop = _entry_type("SfxSharedTimerClickStop", _clad_enum.Sfx_Shared_Timer_Click_Stop) | |||
#: Plays timer end sound | |||
SfxSharedTimerEnd = _entry_type("SfxSharedTimerEnd", _clad_enum.Sfx_Shared_Timer_End) | |||
#: Stops active plays of timer end sound | |||
SfxSharedTimerEndStop = _entry_type("SfxSharedTimerEndStop", _clad_enum.Sfx_Shared_Timer_End_Stop) | |||
#: Plays timer warning sound | |||
SfxSharedTimerWarning = _entry_type("SfxSharedTimerWarning", _clad_enum.Sfx_Shared_Timer_Warning) | |||
#: Stop all active plays of timer warning sound | |||
SfxSharedTimerWarningStop = _entry_type("SfxSharedTimerWarningStop", _clad_enum.Sfx_Shared_Timer_Warning_Stop) | |||
#: Plays a fun music sound (that loops indefinitely). | |||
MusicFunLoop = _entry_type("Music_Fun_Loop", _clad_enum.Music_Fun_Loop) | |||
#: Stops all active plays of the fun music sound. | |||
MusicFunLoopStop = _entry_type("Music_Fun_Loop_Stop", _clad_enum.Music_Fun_Loop_Stop) | |||
#: Plays the putt-hole-success sound. | |||
SfxPuttHoleSuccess = _entry_type("Sfx_Putt_Hole_Success", _clad_enum.Sfx_Putt_Hole_Success) | |||
#: Stops all active plays of the putt-hole-success sound. | |||
SfxPuttHoleSuccessStop = _entry_type("Sfx_Putt_Hole_Success_Stop", _clad_enum.Sfx_Putt_Hole_Success_Stop) | |||
#: Plays alien invasion sound. | |||
Sfx_Alien_Invasion_Ufo = _entry_type("Sfx_Alien_Invasion_Ufo", _clad_enum.Sfx_Alien_Invasion_Ufo) | |||
#: Stops all active plays of the alien invasion sound. | |||
Sfx_Alien_Invasion_Ufo_Stop = _entry_type("Sfx_Alien_Invasion_Ufo_Stop", _clad_enum.Sfx_Alien_Invasion_Ufo_Stop) | |||
#: Plays brick bash sound. | |||
Sfx_Brick_Bash = _entry_type("Sfx_Brick_Bash", _clad_enum.Sfx_Brick_Bash) | |||
#: Stops all active plays of the brick bash sound. | |||
Sfx_Brick_Bash_Stop = _entry_type("Sfx_Brick_Bash_Stop", _clad_enum.Sfx_Brick_Bash_Stop) | |||
#: Plays constellation star sound. | |||
Sfx_Constellation_Star = _entry_type("Sfx_Constellation_Star", _clad_enum.Sfx_Constellation_Star) | |||
#: Stops all active plays of the constellation star sound. | |||
Sfx_Constellation_Star_Stop = _entry_type("Sfx_Constellation_Star_Stop", _clad_enum.Sfx_Constellation_Star_Stop) | |||
#: Plays egg cracking sound. | |||
Sfx_Egg_Decorating_Crack = _entry_type("Sfx_Egg_Decorating_Crack", _clad_enum.Sfx_Egg_Decorating_Crack) | |||
#: Stops all active plays of the egg cracking sound. | |||
Sfx_Egg_Decorating_Crack_Stop = _entry_type("Sfx_Egg_Decorating_Crack_Stop", _clad_enum.Sfx_Egg_Decorating_Crack_Stop) | |||
#: Plays fidget spinner loop. | |||
Sfx_Fidget_Spinner_Loop_Play = _entry_type("Sfx_Fidget_Spinner_Loop_Play", _clad_enum.Sfx_Fidget_Spinner_Loop_Play) | |||
#: Stops all the fidget spinned looping sound. | |||
Sfx_Fidget_Spinner_Loop_Stop = _entry_type("Sfx_Fidget_Spinner_Loop_Stop", _clad_enum.Sfx_Fidget_Spinner_Loop_Stop) | |||
#: Plays fidget spinner sound. | |||
Sfx_Fidget_Spinner_Start = _entry_type("Sfx_Fidget_Spinner_Start", _clad_enum.Sfx_Fidget_Spinner_Start) | |||
#: Stops all active plays of the fidget spinner sound. | |||
Sfx_Fidget_Spinner_Start_Stop = _entry_type("Sfx_Fidget_Spinner_Start_Stop", _clad_enum.Sfx_Fidget_Spinner_Start_Stop) | |||
#: Plays flappy sound. | |||
Sfx_Flappy_Increase = _entry_type("Sfx_Flappy_Increase", _clad_enum.Sfx_Flappy_Increase) | |||
#: Stops all active plays of the flappy sound. | |||
Sfx_Flappy_Increase_Stop = _entry_type("Sfx_Flappy_Increase_Stop", _clad_enum.Sfx_Flappy_Increase_Stop) | |||
#: Plays morse code dash sound. | |||
Sfx_Morse_Code_Dash = _entry_type("Sfx_Morse_Code_Dash", _clad_enum.Sfx_Morse_Code_Dash) | |||
#: Stops all active plays of the morse code dash sound. | |||
Sfx_Morse_Code_Dash_Stop = _entry_type("Sfx_Morse_Code_Dash_Stop", _clad_enum.Sfx_Morse_Code_Dash_Stop) | |||
#: Plays morse code dot sound. | |||
Sfx_Morse_Code_Dot = _entry_type("Sfx_Morse_Code_Dot", _clad_enum.Sfx_Morse_Code_Dot) | |||
#: Stops all active plays of the morse code dot sound. | |||
Sfx_Morse_Code_Dot_Stop = _entry_type("Sfx_Morse_Code_Dot_Stop", _clad_enum.Sfx_Morse_Code_Dot_Stop) | |||
#: Plays morse code silent sound. | |||
Sfx_Morse_Code_Silent = _entry_type("Sfx_Morse_Code_Silent", _clad_enum.Sfx_Morse_Code_Silent) | |||
#: Stops all active plays of the morse code silent sound. | |||
Sfx_Morse_Code_Silent_Stop = _entry_type("Sfx_Morse_Code_Silent_Stop", _clad_enum.Sfx_Morse_Code_Silent_Stop) | |||
#: Plays paddle ball bounce sound. | |||
Sfx_Paddle_Ball_Bounce = _entry_type("Sfx_Paddle_Ball_Bounce", _clad_enum.Sfx_Paddle_Ball_Bounce) | |||
#: Stops all active plays of the paddle ball bounce sound. | |||
Sfx_Paddle_Ball_Bounce_Stop = _entry_type("Sfx_Paddle_Ball_Bounce_Stop", _clad_enum.Sfx_Paddle_Ball_Bounce_Stop) | |||
#: Plays the first pot of gold sound sound. | |||
Sfx_Pot_O_Gold_Blip_Level1 = _entry_type("Sfx_Pot_O_Gold_Blip_Level1", _clad_enum.Sfx_Pot_O_Gold_Blip_Level1) | |||
#: Stops all active plays of the first pot of gold blip sound. | |||
Sfx_Pot_O_Gold_Blip_Level1_Stop = _entry_type("Sfx_Pot_O_Gold_Blip_Level1_Stop", _clad_enum.Sfx_Pot_O_Gold_Blip_Level1_Stop) | |||
#: Plays the second pot of gold sound sound. | |||
Sfx_Pot_O_Gold_Blip_Level2 = _entry_type("Sfx_Pot_O_Gold_Blip_Level2", _clad_enum.Sfx_Pot_O_Gold_Blip_Level2) | |||
#: Stops all active plays of the second pot of gold blip sound. | |||
Sfx_Pot_O_Gold_Blip_Level2_Stop = _entry_type("Sfx_Pot_O_Gold_Blip_Level2_Stop", _clad_enum.Sfx_Pot_O_Gold_Blip_Level2_Stop) | |||
#: Plays the third pot of gold sound sound. | |||
Sfx_Pot_O_Gold_Blip_Level3 = _entry_type("Sfx_Pot_O_Gold_Blip_Level3", _clad_enum.Sfx_Pot_O_Gold_Blip_Level3) | |||
#: Stops all active plays of the third pot of gold blip sound. | |||
Sfx_Pot_O_Gold_Blip_Level3_Stop = _entry_type("Sfx_Pot_O_Gold_Blip_Level3_Stop", _clad_enum.Sfx_Pot_O_Gold_Blip_Level3_Stop) | |||
AudioEvents._init_class(warn_on_missing_definitions=False) |
@@ -0,0 +1,241 @@ | |||
# Copyright (c) 2016 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. | |||
__all__ = [] | |||
import threading | |||
import asyncio | |||
import concurrent.futures | |||
import functools | |||
import inspect | |||
import traceback | |||
import types | |||
class _MetaBase(type): | |||
'''Metaclass for all Cozmo package classes. | |||
Ensures that all *_factory class attributes are wrapped into a _Factory | |||
descriptor to automatically support synchronous operation. | |||
''' | |||
def __new__(mcs, name, bases, attrs, **kw): | |||
for k, v in attrs.items(): | |||
if k.endswith('_factory'): | |||
# TODO: check type here too | |||
attrs[k] = _Factory(v) | |||
return super().__new__(mcs, name, bases, attrs, **kw) | |||
def __setattr__(cls, name, val): | |||
if name.endswith('_factory'): | |||
cls.__dict__[name].__set__(cls, val) | |||
else: | |||
super().__setattr__(name, val) | |||
class Base(metaclass=_MetaBase): | |||
'''Base class for Cozmo package objects. | |||
*_factory attributes are automatically wrapped into a _Factory descriptor to | |||
support synchronous operation. | |||
''' | |||
# used by SyncFatory | |||
_sync_thread_id = None | |||
_sync_abort_future = None | |||
def __init__(self, _sync_thread_id=None, _sync_abort_future=None, **kw): | |||
# machinery for SyncFactory | |||
if _sync_abort_future is not None: | |||
self._sync_thread_id = threading.get_ident() | |||
else: | |||
self._sync_thread_id = _sync_thread_id | |||
self._sync_abort_future = _sync_abort_future | |||
super().__init__(**kw) | |||
@property | |||
def loop(self): | |||
''':class:`asyncio.BaseEventLoop`: loop instance that this object is registered with.''' | |||
return getattr(self, '_loop', None) | |||
class _Factory: | |||
'''Descriptor to wraps an object factory method. | |||
If the factory is called while the program is running in synchronous mode | |||
then the objects returned by the factory will be wrapped by a _SyncProxy | |||
object, which translates asynchronous responses to synchronous ones | |||
when made outside of the thread the top level object's event loop is running on. | |||
''' | |||
def __init__(self, factory): | |||
self._wrapped_factory = factory | |||
def __get__(self, ins, owner): | |||
sync_thread_id = getattr(ins, '_sync_thread_id', None) | |||
loop = getattr(ins, '_loop', None) | |||
if sync_thread_id: | |||
# Object instance is running in sync mode | |||
return _SyncFactory(self._wrapped_factory, loop, sync_thread_id, ins._sync_abort_future) | |||
# Pass through to the factory. Set loop here as a convenience as all | |||
# Cozmo objects require it by virtue of inheriting from event.Dispatcher | |||
return functools.partial(self._wrapped_factory, loop=loop) | |||
def __set__(self, ins, val): | |||
self._wrapped_factory = val | |||
def _SyncFactory(f, loop, thread_id, sync_abort_future): | |||
'''Instantiates a class by calling a factory function and then wrapping it with _SyncProxy''' | |||
def factory(*a, **kw): | |||
kw['_sync_thread_id'] = thread_id | |||
kw['_sync_abort_future'] = sync_abort_future | |||
if 'loop' not in kw: | |||
kw['loop'] = loop | |||
obj = f(*a, **kw) | |||
return _mkproxy(obj) | |||
return factory | |||
def _mkpt(cls, name): | |||
# create a passthru function | |||
f = getattr(cls, name) | |||
@functools.wraps(f) | |||
def pt(self, *a, **kw): | |||
wrap = self.__wrapped__ | |||
f = object.__getattribute__(wrap, name) | |||
return f(*a, **kw) | |||
return pt | |||
class _SyncProxy: | |||
'''Wraps cozmo objects to provide synchronous access when required. | |||
Each method call and attribute access is passed through to the wrapped object. | |||
If the caller is operating in a different thread to the callee (for example, the | |||
caller is operating outside of the context of the event loop), then any | |||
calls to the wrapped object are dispatched to the event loop running on the | |||
loop's native thread. | |||
Returned co-routines functions and Futures are waited upon until completion. | |||
''' | |||
def __init__(self, wrapped): | |||
self.__wrapped__ = wrapped | |||
def __getattribute__(self, name): | |||
wrapped = object.__getattribute__(self, '__wrapped__') | |||
if name == '__wrapped__': | |||
return wrapped | |||
# if name points to a property, this will execute the property getter | |||
# and return the value, else returns the value according to usual | |||
# lookup rules. | |||
value = object.__getattribute__(wrapped, name) | |||
# determine whether the call is being invoked locally, from within the | |||
# event loop's native thread, or elsewhere (usually the main thread) | |||
thread_id = object.__getattribute__(wrapped, '_sync_thread_id') | |||
is_local_thread = thread_id is None or threading.get_ident() == thread_id | |||
if is_local_thread: | |||
# passthru/no-op if being called from the same thread as the object | |||
# was created from. | |||
return value | |||
if inspect.ismethod(value) and not asyncio.iscoroutinefunction(value): | |||
# Wrap the sync method into a coroutine that can be dispatched | |||
# from the same thread as the main event loop is running in | |||
f = value.__func__ | |||
f = _to_coroutine(f) | |||
value = types.MethodType(f, wrapped) | |||
#value = types.MethodType(f, self) | |||
elif inspect.isfunction(value) and not asyncio.iscoroutinefunction(value): | |||
# Dispatch functions in the main event loop thread too | |||
value = _to_coroutine(value) | |||
if inspect.isawaitable(value): | |||
return _dispatch_coroutine(value, wrapped._loop, wrapped._sync_abort_future) | |||
elif asyncio.iscoroutinefunction(value): | |||
# Wrap coroutine into synchronous dispatch | |||
@functools.wraps(value) | |||
def wrap(*a, **kw): | |||
return _dispatch_coroutine(value(*a, **kw), wrapped._loop, wrapped._sync_abort_future) | |||
return wrap | |||
return value | |||
def __setattr__(self, name, value): | |||
if name == '__wrapped__': | |||
return super().__setattr__(name, value) | |||
wrapped = object.__getattribute__(self, '__wrapped__') | |||
return wrapped.__setattr__(name, value) | |||
def __repr__(self): | |||
wrapped = self.__wrapped__ | |||
return "wrapped-" + object.__getattribute__(wrapped, '__repr__')() | |||
def _to_coroutine(f): | |||
@functools.wraps(f) | |||
async def wrap(*a, **kw): | |||
return f(*a, **kw) | |||
return wrap | |||
def _mkproxy(obj): | |||
'''Create a _SyncProxy for an object.''' | |||
# dynamically generate a class tailored for the wrapped object. | |||
d = {} | |||
cls = obj.__class__ | |||
for name in dir(cls): | |||
if ((name.endswith('__') and name.startswith('__')) | |||
and name not in ('__class__', '__new__', '__init__', '__getattribute__', '__setattr__', '__repr__')): | |||
d[name] = _mkpt(cls, name) | |||
if hasattr(obj, '__aenter__'): | |||
d['__enter__'] = lambda self: self.__wrapper__.__aenter__() | |||
d['__exit__'] = lambda self, *a: self.__wrapper__.__aexit__(*a) | |||
cls = type("_proxy_"+obj.__class__.__name__, (_SyncProxy,), d) | |||
proxy = cls(obj) | |||
obj.__wrapper__ = proxy | |||
return proxy | |||
def _dispatch_coroutine(co, loop, abort_future): | |||
'''Execute a coroutine in a loop's thread and block till completion. | |||
Wraps a co-routine function; calling the function causes the co-routine | |||
to be dispatched in the event loop's thread and blocks until that call completes. | |||
Waits for either the coroutine or abort_future to complete. | |||
abort_future provides the main event loop with a means of triggering a | |||
clean shutdown in the case of an exception. | |||
''' | |||
fut = asyncio.run_coroutine_threadsafe(co, loop) | |||
result = concurrent.futures.wait((fut, abort_future), return_when=concurrent.futures.FIRST_COMPLETED) | |||
result = list(result.done)[0].result() | |||
if getattr(result, '__wrapped__', None) is None: | |||
# If the call retuned the wrapped contents of a _SyncProxy then return | |||
# the enclosing proxy instead to the sync caller | |||
wrapper = getattr(result, '__wrapper__', None) | |||
if wrapper is not None: | |||
result = wrapper | |||
return result |
@@ -0,0 +1,206 @@ | |||
# Copyright (c) 2016 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. | |||
''' | |||
Behaviors represent a task that Cozmo may perform for an | |||
indefinite amount of time. | |||
For example, the "LookAroundInPlace" behavior causes Cozmo to start looking | |||
around him (without driving), which will cause events such as | |||
:class:`cozmo.objects.EvtObjectObserved` to be generated as he comes across | |||
objects. | |||
Behaviors must be explicitly stopped before having the robot do something else | |||
(for example, pick up the object he just observed). | |||
Behaviors are started by a call to :meth:`cozmo.robot.Robot.start_behavior`, | |||
which returns a :class:`Behavior` object. Calling the :meth:`~Behavior.stop` | |||
method on that object terminate the behavior. | |||
The :class:`BehaviorTypes` class in this module holds a list of all available | |||
behaviors. | |||
''' | |||
# __all__ should order by constants, event classes, other classes, functions. | |||
__all__ = ['BEHAVIOR_IDLE', 'BEHAVIOR_REQUESTED', 'BEHAVIOR_RUNNING', | |||
'BEHAVIOR_STOPPED', | |||
'EvtBehaviorRequested', 'EvtBehaviorStarted', 'EvtBehaviorStopped', | |||
'Behavior', 'BehaviorTypes'] | |||
import collections | |||
from . import logger | |||
from . import event | |||
from ._clad import _clad_to_engine_cozmo, CladEnumWrapper | |||
#: string: Behavior idle state (not requested to run) | |||
BEHAVIOR_IDLE = 'behavior_idle' | |||
#: string: Behavior requested state (waiting for engine to start it) | |||
BEHAVIOR_REQUESTED = 'behavior_requested' | |||
#: string: Behavior running state | |||
BEHAVIOR_RUNNING = 'behavior_running' | |||
#: string: Behavior stopped state | |||
BEHAVIOR_STOPPED = 'behavior_stopped' | |||
class EvtBehaviorRequested(event.Event): | |||
'''Triggered when a behavior is requested to start.''' | |||
behavior = 'The Behavior object' | |||
behavior_type_name = 'The behavior type name - equivalent to behavior.type.name' | |||
class EvtBehaviorStarted(event.Event): | |||
'''Triggered when a behavior starts running on the robot.''' | |||
behavior = 'The Behavior object' | |||
behavior_type_name = 'The behavior type name - equivalent to behavior.type.name' | |||
class EvtBehaviorStopped(event.Event): | |||
'''Triggered when a behavior stops.''' | |||
behavior = 'The behavior type object' | |||
behavior_type_name = 'The behavior type name - equivalent to behavior.type.name' | |||
class Behavior(event.Dispatcher): | |||
'''A Behavior instance describes a behavior the robot is currently performing. | |||
Returned by :meth:`cozmo.robot.Robot.start_behavior`. | |||
''' | |||
def __init__(self, robot, behavior_type, is_active=False, **kw): | |||
super().__init__(**kw) | |||
self.robot = robot | |||
self.type = behavior_type | |||
self._state = BEHAVIOR_IDLE | |||
if is_active: | |||
self._state = BEHAVIOR_REQUESTED | |||
self.dispatch_event(EvtBehaviorRequested, behavior=self, behavior_type_name=self.type.name) | |||
def __repr__(self): | |||
return '<%s type="%s">' % (self.__class__.__name__, self.type.name) | |||
def _on_engine_started(self): | |||
if self._state != BEHAVIOR_REQUESTED: | |||
# has not been requested (is an unrelated behavior transition) | |||
if self.is_running: | |||
logger.warning("Behavior '%s' unexpectedly reported started when already running") | |||
return | |||
self._state = BEHAVIOR_RUNNING | |||
self.dispatch_event(EvtBehaviorStarted, behavior=self, behavior_type_name=self.type.name) | |||
def _set_stopped(self): | |||
if not self.is_active: | |||
return | |||
self._state = BEHAVIOR_STOPPED | |||
self.dispatch_event(EvtBehaviorStopped, behavior=self, behavior_type_name=self.type.name) | |||
def stop(self): | |||
'''Requests that the robot stop performing the behavior. | |||
Has no effect if the behavior is not presently active. | |||
''' | |||
if not self.is_active: | |||
return | |||
self.robot._set_none_behavior() | |||
self._set_stopped() | |||
@property | |||
def is_active(self): | |||
'''bool: True if the behavior is currently active and may run on the robot.''' | |||
return self._state == BEHAVIOR_REQUESTED or self._state == BEHAVIOR_RUNNING | |||
@property | |||
def is_running(self): | |||
'''bool: True if the behavior is currently running on the robot.''' | |||
return self._state == BEHAVIOR_RUNNING | |||
@property | |||
def is_completed(self): | |||
return self._state == BEHAVIOR_STOPPED | |||
async def wait_for_started(self, timeout=5): | |||
'''Waits for the behavior to start. | |||
Args: | |||
timeout (int or None): Maximum time in seconds to wait for the event. | |||
Pass None to wait indefinitely. If a behavior can run it should | |||
usually start within ~0.2 seconds. | |||
Raises: | |||
:class:`asyncio.TimeoutError` | |||
''' | |||
if self.is_running or self.is_completed: | |||
# Already started running | |||
return | |||
await self.wait_for(EvtBehaviorStarted, timeout=timeout) | |||
async def wait_for_completed(self, timeout=None): | |||
'''Waits for the behavior to complete. | |||
Args: | |||
timeout (int or None): Maximum time in seconds to wait for the event. | |||
Pass None to wait indefinitely. | |||
Raises: | |||
:class:`asyncio.TimeoutError` | |||
''' | |||
if self.is_completed: | |||
# Already complete | |||
return | |||
# Wait for behavior to start first - it can't complete without starting, | |||
# and if it doesn't start within a fraction of a second it probably | |||
# never will | |||
await self.wait_for_started() | |||
await self.wait_for(EvtBehaviorStopped, timeout=timeout) | |||
_BehaviorType = collections.namedtuple('_BehaviorType', ['name', 'id']) | |||
class BehaviorTypes(CladEnumWrapper): | |||
'''Defines all executable robot behaviors. | |||
For use with :meth:`cozmo.robot.Robot.start_behavior`. | |||
''' | |||
_clad_enum = _clad_to_engine_cozmo.ExecutableBehaviorType | |||
_entry_type = _BehaviorType | |||
#: Turn and move head, but don't drive, with Cozmo's head angled | |||
#: upwards where faces are likely to be. | |||
FindFaces = _entry_type("FindFaces", _clad_enum.FindFaces) | |||
#: Knock over a stack of cubes. | |||
KnockOverCubes = _entry_type("KnockOverCubes", _clad_enum.KnockOverCubes) | |||
#: Turn and move head, but don't drive, to see what is around Cozmo. | |||
LookAroundInPlace = _entry_type("LookAroundInPlace", _clad_enum.LookAroundInPlace) | |||
#: Tries to "pounce" (drive forward and lower lift) when it detects | |||
#: nearby motion on the ground plane. | |||
PounceOnMotion = _entry_type("PounceOnMotion", _clad_enum.PounceOnMotion) | |||
#: Roll a block, regardless of orientation. | |||
RollBlock = _entry_type("RollBlock", _clad_enum.RollBlock) | |||
#: Pickup one block, and stack it onto another block. | |||
StackBlocks = _entry_type("StackBlocks", _clad_enum.StackBlocks) | |||
# Enroll a Face - for internal use by Face.name_face (requires additional pre/post setup) | |||
_EnrollFace = _entry_type("EnrollFace", _clad_enum.EnrollFace) | |||
# This enum deliberately only exposes a sub-set of working behaviors | |||
BehaviorTypes._init_class(warn_on_missing_definitions=False, add_missing_definitions=False) |
@@ -0,0 +1,579 @@ | |||
# Copyright (c) 2016 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. | |||
'''Support for Cozmo's camera. | |||
Cozmo has a built-in camera which he uses to observe the world around him. | |||
The :class:`Camera` class defined in this module is made available as | |||
:attr:`cozmo.world.World.camera` and can be used to enable/disable image | |||
sending, enable/disable color images, modify various camera settings, | |||
read the robot's unique camera calibration settings, as well as observe | |||
raw unprocessed images being sent by the robot. | |||
Generally, however, it is more useful to observe | |||
:class:`cozmo.world.EvtNewCameraImage` events, which include the raw camera | |||
images along with annotated images, which can illustrate objects the robot | |||
has identified. | |||
''' | |||
# __all__ should order by constants, event classes, other classes, functions. | |||
__all__ = ['EvtNewRawCameraImage', 'EvtRobotObservedMotion', 'CameraConfig', 'Camera'] | |||
import functools | |||
import io | |||
_img_processing_available = True | |||
try: | |||
import numpy as np | |||
from PIL import Image | |||
except ImportError as exc: | |||
np = None | |||
_img_processing_available = exc | |||
from . import event | |||
from . import logger | |||
from . import util | |||
from ._clad import _clad_to_engine_iface, _clad_to_engine_cozmo, _clad_to_game_cozmo | |||
_clad_res = _clad_to_game_cozmo.ImageResolution | |||
RESOLUTIONS = { | |||
_clad_res.VerificationSnapshot: (16, 16), | |||
_clad_res.QQQQVGA: (40, 30), | |||
_clad_res.QQQVGA: (80, 60), | |||
_clad_res.QQVGA: (160, 120), | |||
_clad_res.QVGA: (320, 240), | |||
_clad_res.CVGA: (400, 296), | |||
_clad_res.VGA: (640, 480), | |||
_clad_res.SVGA: (800, 600), | |||
_clad_res.XGA: (1024, 768), | |||
_clad_res.SXGA: (1280, 960), | |||
_clad_res.UXGA: (1600, 1200), | |||
_clad_res.QXGA: (2048, 1536), | |||
_clad_res.QUXGA: (3200, 2400) | |||
} | |||
# wrap functions/methods that require NumPy or PIL with this | |||
# decorator to ensure they fail with a useful error if those packages | |||
# are not loaded. | |||
def _require_img_processing(f): | |||
@functools.wraps(f) | |||
def wrapper(*a, **kw): | |||
if _img_processing_available is not True: | |||
raise ImportError("Camera image processing not available: %s" % _img_processing_available) | |||
return f(*a, **kw) | |||
return wrapper | |||
class EvtNewRawCameraImage(event.Event): | |||
'''Dispatched when a new raw image is received from the robot's camera. | |||
See also :class:`~cozmo.world.EvtNewCameraImage` which provides access | |||
to both the raw image and a scaled and annotated version. | |||
''' | |||
image = 'A PIL.Image.Image object' | |||
class EvtRobotObservedMotion(event.Event): | |||
'''Generated when the robot observes motion.''' | |||
timestamp = "Robot timestamp for when movement was observed" | |||
img_area = "Area of the supporting region for the point, as a fraction of the image" | |||
img_pos = "Centroid of observed motion, relative to top-left corner" | |||
ground_area = "Area of the supporting region for the point, as a fraction of the ground ROI" | |||
ground_pos = "Approximate coordinates of observed motion on the ground, relative to robot, in mm" | |||
has_top_movement = "Movement detected near the top of the robot's view" | |||
top_img_pos = "Coordinates of the centroid of observed motion, relative to top-left corner" | |||
has_left_movement = "Movement detected near the left edge of the robot's view" | |||
left_img_pos = "Coordinates of the centroid of observed motion, relative to top-left corner" | |||
has_right_movement = "Movement detected near the right edge of the robot's view" | |||
right_img_pos = "Coordinates of the centroid of observed motion, relative to top-left corner" | |||
class CameraConfig: | |||
"""The fixed properties for Cozmo'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 | | |||
+--------------+--------------+---------------+ | |||
""" | |||
def __init__(self, | |||
focal_length_x: float, | |||
focal_length_y: float, | |||
center_x: float, | |||
center_y: float, | |||
fov_x_degrees: float, | |||
fov_y_degrees: 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_degrees) | |||
self._fov_y = util.degrees(fov_y_degrees) | |||
self._min_exposure_time_ms = min_exposure_time_ms | |||
self._max_exposure_time_ms = max_exposure_time_ms | |||
self._min_gain = min_gain | |||
self._max_gain = max_gain | |||
@classmethod | |||
def _create_from_clad(cls, cs): | |||
return cls(cs.focalLengthX, cs.focalLengthY, | |||
cs.centerX, cs.centerY, | |||
cs.fovX, cs.fovY, | |||
cs.minCameraExposureTime_ms, cs.maxCameraExposureTime_ms, | |||
cs.minCameraGain, cs.maxCameraGain) | |||
# Fixed camera properties (calibrated for each robot at the factory). | |||
@property | |||
def focal_length(self): | |||
''':class:`cozmo.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:`cozmo.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 at the factory. It is in floating point | |||
pixel values e.g. <155.11, 111.40>. | |||
''' | |||
return self._center | |||
@property | |||
def fov_x(self): | |||
''':class:`cozmo.util.Angle`: The x (horizontal) field of view.''' | |||
return self._fov_x | |||
@property | |||
def fov_y(self): | |||
''':class:`cozmo.util.Angle`: The y (vertical) field of view.''' | |||
return self._fov_y | |||
# The fixed range of values supported for this camera. | |||
@property | |||
def min_exposure_time_ms(self): | |||
'''int: The minimum supported exposure time in milliseconds.''' | |||
return self._min_exposure_time_ms | |||
@property | |||
def max_exposure_time_ms(self): | |||
'''int: The maximum supported exposure time in milliseconds.''' | |||
return self._max_exposure_time_ms | |||
@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 | |||
class Camera(event.Dispatcher): | |||
'''Represents Cozmo's camera. | |||
The Camera object receives images from Cozmo's camera and emits | |||
EvtNewRawCameraImage events. | |||
The :class:`cozmo.world.World` instance observes the camera and provides | |||
more useful methods for accessing the camera images. | |||
.. important:: | |||
The camera will not receive any image data unless you | |||
explicitly enable it by setting :attr:`Camera.image_stream_enabled` | |||
to ``True`` | |||
''' | |||
def __init__(self, robot, **kw): | |||
super().__init__(**kw) | |||
self.robot = robot | |||
self._image_stream_enabled = None | |||
self._color_image_enabled = None | |||
self._config = None # type: CameraConfig | |||
self._gain = 0.0 | |||
self._exposure_ms = 0 | |||
self._auto_exposure_enabled = True | |||
if np is None: | |||
logger.warning("Camera image processing not available due to missing NumPy or Pillow packages: %s" % _img_processing_available) | |||
else: | |||
# set property to ensure clad initialization is sent. | |||
self.image_stream_enabled = False | |||
self.color_image_enabled = False | |||
self._reset_partial_state() | |||
def enable_auto_exposure(self, enable_auto_exposure = True): | |||
'''Enable auto exposure on Cozmo's Camera. | |||
Enable auto exposure on Cozmo'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. | |||
Args: | |||
enable_auto_exposure (bool): whether the camera should automcatically adjust exposure | |||
''' | |||
msg = _clad_to_engine_iface.SetCameraSettings(enableAutoExposure = enable_auto_exposure) | |||
self.robot.conn.send_msg(msg) | |||
def set_manual_exposure(self, exposure_ms, gain): | |||
'''Set manual exposure values for Cozmo's Camera. | |||
Disable auto exposure on Cozmo's camera and force the specified exposure | |||
time and gain values. | |||
Args: | |||
exposure_ms (int): The desired exposure time in milliseconds. | |||
Must be within the robot's | |||
:attr:`~cozmo.camera.Camera.config` exposure range from | |||
:attr:`~cozmo.camera.CameraConfig.min_exposure_time_ms` to | |||
:attr:`~cozmo.camera.CameraConfig.max_exposure_time_ms` | |||
gain (float): The desired gain value. | |||
Must be within the robot's | |||
:attr:`~cozmo.camera.Camera.camera_config` gain range from | |||
:attr:`~cozmo.camera.CameraConfig.min_gain` to | |||
:attr:`~cozmo.camera.CameraConfig.max_gain` | |||
Raises: | |||
:class:`ValueError` if supplied an out-of-range exposure or gain. | |||
''' | |||
cam = self.config | |||
if (exposure_ms < cam.min_exposure_time_ms) or (exposure_ms > cam.max_exposure_time_ms): | |||
raise ValueError('exposure_ms %s out of range %s..%s' % | |||
(exposure_ms, cam.min_exposure_time_ms, cam.max_exposure_time_ms)) | |||
if (gain < cam.min_gain) or (gain > cam.max_gain): | |||
raise ValueError('gain %s out of range %s..%s' % | |||
(gain, cam.min_gain, cam.max_gain)) | |||
msg = _clad_to_engine_iface.SetCameraSettings(enableAutoExposure=False, | |||
exposure_ms=exposure_ms, | |||
gain=gain) | |||
self.robot.conn.send_msg(msg) | |||
#### Private Methods #### | |||
def _reset_partial_state(self): | |||
self._partial_data = None | |||
self._partial_image_id = None | |||
self._partial_invalid = False | |||
self._partial_size = 0 | |||
self._partial_metadata = None | |||
self._last_chunk_id = -1 | |||
def _set_config(self, clad_config): | |||
self._config = CameraConfig._create_from_clad(clad_config) | |||
#### Properties #### | |||
@property | |||
@_require_img_processing | |||
def image_stream_enabled(self): | |||
'''bool: Set to true to receive camera images from the robot.''' | |||
if np is None: | |||
return False | |||
return self._image_stream_enabled | |||
@image_stream_enabled.setter | |||
@_require_img_processing | |||
def image_stream_enabled(self, enabled): | |||
if self._image_stream_enabled == enabled: | |||
return | |||
self._image_stream_enabled = enabled | |||
if enabled: | |||
image_send_mode = _clad_to_engine_cozmo.ImageSendMode.Stream | |||
else: | |||
image_send_mode = _clad_to_engine_cozmo.ImageSendMode.Off | |||
msg = _clad_to_engine_iface.ImageRequest(mode=image_send_mode) | |||
self.robot.conn.send_msg(msg) | |||
@property | |||
@_require_img_processing | |||
def color_image_enabled(self): | |||
'''bool: Set to true to receive color images from the robot.''' | |||
if np is None: | |||
return False | |||
return self._color_image_enabled | |||
@color_image_enabled.setter | |||
@_require_img_processing | |||
def color_image_enabled(self, enabled): | |||
if self._color_image_enabled == enabled: | |||
return | |||
self._color_image_enabled = enabled | |||
msg = _clad_to_engine_iface.EnableColorImages(enable = enabled) | |||
self.robot.conn.send_msg(msg) | |||
@property | |||
def config(self): | |||
''':class:`cozmo.camera.CameraConfig`: The read-only config/calibration for the camera''' | |||
return self._config | |||
@property | |||
def is_auto_exposure_enabled(self): | |||
'''bool: True if auto exposure is currently enabled | |||
If auto exposure is enabled the `gain` and `exposure_ms` | |||
values will constantly be updated by Cozmo. | |||
''' | |||
return self._auto_exposure_enabled | |||
@property | |||
def gain(self): | |||
'''float: The current camera gain setting.''' | |||
return self._gain | |||
@property | |||
def exposure_ms(self): | |||
'''int: The current camera exposure setting in milliseconds.''' | |||
return self._exposure_ms | |||
#### Private Event Handlers #### | |||
def _recv_msg_image_chunk(self, evt, *, msg): | |||
if np is None: | |||
return | |||
if self._partial_image_id is not None and msg.chunkId == 0: | |||
if not self._partial_invalid: | |||
logger.debug("Lost final chunk of image; discarding") | |||
self._partial_image_id = None | |||
if self._partial_image_id is None: | |||
if msg.chunkId != 0: | |||
if not self._partial_invalid: | |||
logger.debug("Received chunk of broken image") | |||
self._partial_invalid = True | |||
return | |||
# discard any previous in-progress image | |||
self._reset_partial_state() | |||
self._partial_image_id = msg.imageId | |||
self._partial_metadata = msg | |||
max_size = msg.imageChunkCount * _clad_to_game_cozmo.ImageConstants.IMAGE_CHUNK_SIZE | |||
width, height = RESOLUTIONS[msg.resolution] | |||
max_size = width * height * 3 # 3 bytes (RGB) per pixel | |||
self._partial_data = np.empty(max_size, dtype=np.uint8) | |||
if msg.chunkId != (self._last_chunk_id + 1) or msg.imageId != self._partial_image_id: | |||
logger.debug("Image missing chunks; discarding (last_chunk_id=%d partial_image_id=%s)", | |||
self._last_chunk_id, self._partial_image_id) | |||
self._reset_partial_state() | |||
self._partial_invalid = True | |||
return | |||
offset = self._partial_size | |||
self._partial_data[offset:offset+len(msg.data)] = msg.data | |||
self._partial_size += len(msg.data) | |||
self._last_chunk_id = msg.chunkId | |||
if msg.chunkId == (msg.imageChunkCount - 1): | |||
self._process_completed_image() | |||
self._reset_partial_state() | |||
def _recv_msg_current_camera_params(self, evt, *, msg): | |||
self._gain = msg.cameraGain | |||
self._exposure_ms = msg.exposure_ms | |||
self._auto_exposure_enabled = msg.autoExposureEnabled | |||
def _recv_msg_robot_observed_motion(self, evt, *, msg): | |||
self.dispatch_event(EvtRobotObservedMotion, | |||
timestamp=msg.timestamp, | |||
img_area=msg.img_area, | |||
img_pos=util.Vector2(msg.img_x, msg.img_y), | |||
ground_area=msg.ground_area, | |||
ground_pos=util.Vector2(msg.ground_x, msg.ground_y), | |||
has_top_movement=(msg.top_img_area > 0), | |||
top_img_pos=util.Vector2(msg.top_img_x, msg.top_img_y), | |||
has_left_movement=(msg.left_img_area > 0), | |||
left_img_pos=util.Vector2(msg.left_img_x, msg.left_img_y), | |||
has_right_movement=(msg.right_img_area > 0), | |||
right_img_pos=util.Vector2(msg.right_img_x, msg.right_img_y)) | |||
def _process_completed_image(self): | |||
data = self._partial_data[0:self._partial_size] | |||
# The first byte of the image is whether or not it is in color | |||
is_color_image = data[0] != 0 | |||
if self._partial_metadata.imageEncoding == _clad_to_game_cozmo.ImageEncoding.JPEGMinimizedGray: | |||
width, height = RESOLUTIONS[self._partial_metadata.resolution] | |||
if is_color_image: | |||
# Color images are half width | |||
width = width // 2 | |||
data = _minicolor_to_jpeg(data, width, height) | |||
else: | |||
data = _minigray_to_jpeg(data, width, height) | |||
image = Image.open(io.BytesIO(data)).convert('RGB') | |||
# Color images need to be resized to the proper resolution | |||
if is_color_image: | |||
size = RESOLUTIONS[self._partial_metadata.resolution] | |||
image = image.resize(size) | |||
self._latest_image = image | |||
self.dispatch_event(EvtNewRawCameraImage, image=image) | |||
#### Public Event Handlers #### | |||
@_require_img_processing | |||
def _minigray_to_jpeg(minigray, width, height): | |||
"Converts miniGrayToJpeg format to normal jpeg format" | |||
#This should be 'exactly' what is done in the miniGrayToJpeg function in encodedImage.cpp | |||
header50 = np.array([ | |||
0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46, 0x49, 0x46, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, | |||
0x00, 0x01, 0x00, 0x00, 0xFF, 0xDB, 0x00, 0x43, 0x00, 0x10, 0x0B, 0x0C, 0x0E, 0x0C, 0x0A, 0x10, #// 0x19 = QTable | |||
0x0E, 0x0D, 0x0E, 0x12, 0x11, 0x10, 0x13, 0x18, 0x28, 0x1A, 0x18, 0x16, 0x16, 0x18, 0x31, 0x23, | |||
0x25, 0x1D, 0x28, 0x3A, 0x33, 0x3D, 0x3C, 0x39, 0x33, 0x38, 0x37, 0x40, 0x48, 0x5C, 0x4E, 0x40, | |||
0x44, 0x57, 0x45, 0x37, 0x38, 0x50, 0x6D, 0x51, 0x57, 0x5F, 0x62, 0x67, 0x68, 0x67, 0x3E, 0x4D, | |||
#//0x71, 0x79, 0x70, 0x64, 0x78, 0x5C, 0x65, 0x67, 0x63, 0xFF, 0xC0, 0x00, 0x0B, 0x08, 0x00, 0xF0, #// 0x5E = Height x Width | |||
0x71, 0x79, 0x70, 0x64, 0x78, 0x5C, 0x65, 0x67, 0x63, 0xFF, 0xC0, 0x00, 0x0B, 0x08, 0x01, 0x28, #// 0x5E = Height x Width | |||
#//0x01, 0x40, 0x01, 0x01, 0x11, 0x00, 0xFF, 0xC4, 0x00, 0xD2, 0x00, 0x00, 0x01, 0x05, 0x01, 0x01, | |||
0x01, 0x90, 0x01, 0x01, 0x11, 0x00, 0xFF, 0xC4, 0x00, 0xD2, 0x00, 0x00, 0x01, 0x05, 0x01, 0x01, | |||
0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, | |||
0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x10, 0x00, 0x02, 0x01, 0x03, 0x03, 0x02, 0x04, 0x03, | |||
0x05, 0x05, 0x04, 0x04, 0x00, 0x00, 0x01, 0x7D, 0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12, | |||
0x21, 0x31, 0x41, 0x06, 0x13, 0x51, 0x61, 0x07, 0x22, 0x71, 0x14, 0x32, 0x81, 0x91, 0xA1, 0x08, | |||
0x23, 0x42, 0xB1, 0xC1, 0x15, 0x52, 0xD1, 0xF0, 0x24, 0x33, 0x62, 0x72, 0x82, 0x09, 0x0A, 0x16, | |||
0x17, 0x18, 0x19, 0x1A, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, | |||
0x3A, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4A, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, | |||
0x5A, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6A, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, | |||
0x7A, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8A, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, | |||
0x99, 0x9A, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6, | |||
0xB7, 0xB8, 0xB9, 0xBA, 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7, 0xC8, 0xC9, 0xCA, 0xD2, 0xD3, 0xD4, | |||
0xD5, 0xD6, 0xD7, 0xD8, 0xD9, 0xDA, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8, 0xE9, 0xEA, | |||
0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8, 0xF9, 0xFA, 0xFF, 0xDA, 0x00, 0x08, 0x01, 0x01, | |||
0x00, 0x00, 0x3F, 0x00 | |||
], dtype=np.uint8) | |||
return _mini_to_jpeg_helper(minigray, width, height, header50) | |||
@_require_img_processing | |||
def _minicolor_to_jpeg(minicolor, width, height): | |||
"Converts miniColorToJpeg format to normal jpeg format" | |||
#This should be 'exactly' what is done in the miniColorToJpeg function in encodedImage.cpp | |||
header = np.array([ | |||
0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46, 0x49, 0x46, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, | |||
0x00, 0x01, 0x00, 0x00, 0xFF, 0xDB, 0x00, 0x43, 0x00, 0x10, 0x0B, 0x0C, 0x0E, 0x0C, 0x0A, 0x10, # 0x19 = QTable | |||
0x0E, 0x0D, 0x0E, 0x12, 0x11, 0x10, 0x13, 0x18, 0x28, 0x1A, 0x18, 0x16, 0x16, 0x18, 0x31, 0x23, | |||
0x25, 0x1D, 0x28, 0x3A, 0x33, 0x3D, 0x3C, 0x39, 0x33, 0x38, 0x37, 0x40, 0x48, 0x5C, 0x4E, 0x40, | |||
0x44, 0x57, 0x45, 0x37, 0x38, 0x50, 0x6D, 0x51, 0x57, 0x5F, 0x62, 0x67, 0x68, 0x67, 0x3E, 0x4D, | |||
0x71, 0x79, 0x70, 0x64, 0x78, 0x5C, 0x65, 0x67, 0x63, 0xFF, 0xC0, 0x00, 17, # 8+3*components | |||
0x08, 0x00, 0xF0, # 0x5E = Height x Width | |||
0x01, 0x40, | |||
0x03, # 3 components | |||
0x01, 0x21, 0x00, # Y 2x1 res | |||
0x02, 0x11, 0x00, # Cb | |||
0x03, 0x11, 0x00, # Cr | |||
0xFF, 0xC4, 0x00, 0xD2, 0x00, 0x00, 0x01, 0x05, 0x01, 0x01, | |||
0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, | |||
0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x10, 0x00, 0x02, 0x01, 0x03, 0x03, 0x02, 0x04, 0x03, | |||
0x05, 0x05, 0x04, 0x04, 0x00, 0x00, 0x01, 0x7D, 0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12, | |||
0x21, 0x31, 0x41, 0x06, 0x13, 0x51, 0x61, 0x07, 0x22, 0x71, 0x14, 0x32, 0x81, 0x91, 0xA1, 0x08, | |||
0x23, 0x42, 0xB1, 0xC1, 0x15, 0x52, 0xD1, 0xF0, 0x24, 0x33, 0x62, 0x72, 0x82, 0x09, 0x0A, 0x16, | |||
0x17, 0x18, 0x19, 0x1A, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, | |||
0x3A, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4A, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, | |||
0x5A, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6A, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, | |||
0x7A, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8A, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, | |||
0x99, 0x9A, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6, | |||
0xB7, 0xB8, 0xB9, 0xBA, 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7, 0xC8, 0xC9, 0xCA, 0xD2, 0xD3, 0xD4, | |||
0xD5, 0xD6, 0xD7, 0xD8, 0xD9, 0xDA, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8, 0xE9, 0xEA, | |||
0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8, 0xF9, 0xFA, | |||
0xFF, 0xDA, 0x00, 12, | |||
0x03, # 3 components | |||
0x01, 0x00, # Y | |||
0x02, 0x00, # Cb same AC/DC | |||
0x03, 0x00, # Cr same AC/DC | |||
0x00, 0x3F, 0x00 | |||
], dtype=np.uint8) | |||
return _mini_to_jpeg_helper(minicolor, width, height, header) | |||
@_require_img_processing | |||
def _mini_to_jpeg_helper(mini, width, height, header): | |||
bufferIn = mini.tolist() | |||
currLen = len(mini) | |||
headerLength = len(header) | |||
# For worst case expansion | |||
bufferOut = np.array([0] * (currLen*2 + headerLength), dtype=np.uint8) | |||
for i in range(headerLength): | |||
bufferOut[i] = header[i] | |||
bufferOut[0x5e] = height >> 8 | |||
bufferOut[0x5f] = height & 0xff | |||
bufferOut[0x60] = width >> 8 | |||
bufferOut[0x61] = width & 0xff | |||
# Remove padding at the end | |||
while (bufferIn[currLen-1] == 0xff): | |||
currLen -= 1 | |||
off = headerLength | |||
for i in range(currLen-1): | |||
bufferOut[off] = bufferIn[i+1] | |||
off += 1 | |||
if (bufferIn[i+1] == 0xff): | |||
bufferOut[off] = 0 | |||
off += 1 | |||
bufferOut[off] = 0xff | |||
off += 1 | |||
bufferOut[off] = 0xD9 | |||
bufferOut[:off] | |||
return np.asarray(bufferOut) |
@@ -0,0 +1,113 @@ | |||
# Copyright (c) 2016 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. | |||
__all__ = [] | |||
import asyncio | |||
import struct | |||
import sys | |||
from threading import Lock | |||
from . import logger_protocol | |||
LOG_ALL = 'all' | |||
if sys.byteorder != 'little': | |||
raise ImportError("Cozmo SDK doesn't support byte order '%s' - contact Anki support to request this", sys.byteorder) | |||
class CLADProtocol(asyncio.Protocol): | |||
'''Low level CLAD codec''' | |||
_send_mutex = Lock() | |||
clad_decode_union = None | |||
clad_encode_union = None | |||
_clad_log_which = None | |||
def __init__(self): | |||
super().__init__() | |||
self._buf = bytearray() | |||
self._abort_connection = False # abort connection on failed handshake, ignore subsequent messages! | |||
def connection_made(self, transport): | |||
self.transport = transport | |||
logger_protocol.debug('Connected to transport') | |||
def connection_lost(self, exc): | |||
logger_protocol.debug("Connnection to transport lost: %s" % exc) | |||
def data_received(self, data): | |||
self._buf.extend(data) | |||
# pull clad messages out | |||
while not self._abort_connection: | |||
msg = self.decode_msg() | |||
# must compare msg against None, not just "if not msg" as the latter | |||
# would match against any message with len==0 (which is the case | |||
# for deliberately empty messages where the tag alone is the signal). | |||
if msg is None: | |||
return | |||
name = msg.tag_name | |||
if self._clad_log_which is LOG_ALL or (self._clad_log_which is not None and name in self._clad_log_which): | |||
logger_protocol.debug('RECV %s', msg._data) | |||
self.msg_received(msg) | |||
def decode_msg(self): | |||
if len(self._buf) < 2: | |||
return None | |||
# TODO: handle error | |||
# messages are prefixed by a 2 byte length | |||
msg_size = struct.unpack_from('H', self._buf)[0] | |||
if len(self._buf) < 2 + msg_size: | |||
return None | |||
buf, self._buf = self._buf[2:2+msg_size], self._buf[2+msg_size:] | |||
try: | |||
return self.clad_decode_union.unpack(buf) | |||
except ValueError as e: | |||
logger_protocol.warn("Failed to decode CLAD message for buflen=%d: %s", len(buf), e) | |||
def eof_received(self): | |||
logger_protocol.info("EOF received on connection") | |||
def send_msg(self, msg, **params): | |||
if self.transport.is_closing(): | |||
return | |||
name = msg.__class__.__name__ | |||
msg = self.clad_encode_union(**{name: msg}) | |||
msg_buf = msg.pack() | |||
msg_size = struct.pack('H', len(msg_buf)) | |||
self._send_mutex.acquire() | |||
try: | |||
self.transport.write(msg_size) | |||
self.transport.write(msg_buf) | |||
if self._clad_log_which is LOG_ALL or (self._clad_log_which is not None and name in self._clad_log_which): | |||
logger_protocol.debug("SENT %s", msg) | |||
finally: | |||
self._send_mutex.release() | |||
def send_msg_new(self, msg): | |||
name = msg.__class__.__name__ | |||
return self.send_msg(name, msg) | |||
def msg_received(self, msg): | |||
pass | |||
@@ -0,0 +1,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 |
@@ -0,0 +1,618 @@ | |||
# Copyright (c) 2016 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. | |||
'''Event dispatch system. | |||
The SDK is based around the dispatch and observation of events. | |||
Objects inheriting from the :class:`Dispatcher` generate and | |||
dispatch events as the state of the robot and its world are updated. | |||
For example the :class:`cozmo.objects.LightCube` class generates an | |||
:class:`~cozmo.objects.EvtObjectTapped` event anytime the cube the object | |||
represents is tapped. | |||
The event can be observed in a number of different ways: | |||
#. By calling the :meth:`~Dispatcher.wait_for` method on the object to observe. | |||
This will wait until the specific event has been sent to that object and | |||
return the generated event. | |||
#. By calling :meth:`~Dispatcher.add_event_handler` on the object | |||
to observe, which will cause the supplied function to be called every time | |||
the specified event occurs (use the :func:`oneshot` decorator | |||
to only have the handler called once) | |||
#. By sub-classing a type and implementing a receiver method. | |||
For example, subclass the :class:`cozmo.objects.LightCube` type and implement `evt_object_tapped`. | |||
Note that the factory attribute would need to be updated on the | |||
generating class for your type to be used by the SDK. | |||
For example, :attr:`~cozmo.world.World.light_cube_factory` in this example. | |||
#. By subclassing a type and implementing a default receiver method. | |||
Events not dispatched to an explicit receiver method are dispatched to | |||
`recv_default_handler`. | |||
Events are dispatched to a target object (by calling :meth:`dispatch_event` | |||
on the receiving object). In line with the above, upon receiving an event, | |||
the object will: | |||
#. Dispatch the event to any handlers which have explicitly registered interest | |||
in the event (or a superclass of the event) via | |||
:meth:`~Dispatcher.add_event_handler` or via :meth:`Dispatcher.wait_for` | |||
#. Dispatch the event to any "children" of the object (see below) | |||
#. Dispatch the event to method handlers on the receiving object, or the | |||
`recv_default_handler` if it has no matching handler | |||
#. Dispatch the event to the parent of the object (if any), and in turn onto | |||
the parent's parents. | |||
Any handler may raise a :class:`~cozmo.exceptions.StopPropogation` exception | |||
to prevent the event reaching any subsequent handlers (but generally should | |||
have no need to do so). | |||
Child objects receive all events that are sent to the originating object | |||
(which may have multiple children). | |||
Originating objects may have one parent object, which receives all events sent | |||
to its child. | |||
For example, :class:`cozmo.robot.Robot` creates a :class:`cozmo.world.World` | |||
object and sets itself as a parent and the World as the child; both receive | |||
events sent to the other. | |||
The World class creates individual :class:`cozmo.objects.ObservableObject` objects | |||
as they are discovered and makes itself a parent, so as to receive all events | |||
sent to the child. However, it does not make those ObservableObject objects children | |||
for the sake of message dispatch as they only need to receive a small subset | |||
of messages the World object receives. | |||
''' | |||
# __all__ should order by constants, event classes, other classes, functions. | |||
__all__ = ['Event', 'Dispatcher', 'Filter', 'Handler', | |||
'oneshot', 'filter_handler', 'wait_for_first'] | |||
import asyncio | |||
import collections | |||
import inspect | |||
import re | |||
import weakref | |||
from . import base | |||
from . import exceptions | |||
from . import logger | |||
# from https://stackoverflow.com/questions/1175208/elegant-python-function-to-convert-camelcase-to-snake-case | |||
_first_cap_re = re.compile('(.)([A-Z][a-z]+)') | |||
_all_cap_re = re.compile('([a-z0-9])([A-Z])') | |||
def _uncamelcase(name): | |||
s1 = _first_cap_re.sub(r'\1_\2', name) | |||
return _all_cap_re.sub(r'\1_\2', s1).lower() | |||
registered_events = {} | |||
active_dispatchers = weakref.WeakSet() | |||
class _rprop: | |||
def __init__(self, value): | |||
self._value = value | |||
def __get__(self, instance, owner): | |||
return self._value | |||
class docstr(str): | |||
@property | |||
def __doc__(self): | |||
return self.__str__() | |||
class _AutoRegister(type): | |||
'''helper to automatically register event classes wherever they're defined | |||
without requiring a class decorator''' | |||
def __new__(mcs, name, bases, attrs, **kw): | |||
if name in ('Event',): | |||
return super().__new__(mcs, name, bases, attrs, **kw) | |||
if not (name.startswith('Evt') or name.startswith('_Evt') or name.startswith('_Msg')): | |||
raise ValueError('Event class names must begin with "Evt (%s)"' % name) | |||
if '__doc__' not in attrs: | |||
raise ValueError('Event classes must have a docstring') | |||
props = set() | |||
for base in bases: | |||
if hasattr(base, '_props'): | |||
props.update(base._props) | |||
newattrs = {'_internal': False} | |||
for k, v in attrs.items(): | |||
if k[0] == '_': | |||
newattrs[k] = v | |||
continue | |||
if k in props: | |||
raise ValueError("Event class %s duplicates property %s defined in superclass" % (mcs, k)) | |||
props.add(k) | |||
newattrs[k] = docstr(v) | |||
newattrs['_props'] = props | |||
newattrs['_props_sorted'] = sorted(props) | |||
if name[0] == '_': | |||
newattrs['_internal'] = True | |||
name = name[1:] | |||
# create a read only property for the event name | |||
newattrs['event_name'] = _rprop(name) | |||
return super().__new__(mcs, name, bases, newattrs, **kw) | |||
def __init__(cls, name, bases, attrs, **kw): | |||
if name in registered_events: | |||
raise ValueError("Duplicate event name %s (%s duplicated by %s)" | |||
% (name, _full_qual_name(cls), _full_qual_name(registered_events[name]))) | |||
registered_events[name] = cls | |||
super().__init__(name, bases, attrs, **kw) | |||
def _full_qual_name(obj): | |||
return obj.__module__ + '.' + obj.__qualname__ | |||
class Event(metaclass=_AutoRegister): | |||
'''An event representing an action that has occurred. | |||
Instances of an Event have attributes set to values passed to the event. | |||
For example, :class:`cozmo.objects.EvtObjectTapped` defines obj and tap_count | |||
parameters which can be accessed as ``evt.obj`` and ``evt.tap_count``. | |||
''' | |||
#_first_raised_by = "The object that generated the event" | |||
#_last_raised_by = "The object that last relayed the event to the dispatched handler" | |||
#pylint: disable=no-member | |||
# Event Metaclass raises "no-member" pylint errors in pylint within this scope. | |||
def __init__(self, **kwargs): | |||
unset = self._props.copy() | |||
for k, v in kwargs.items(): | |||
if k not in self._props: | |||
raise ValueError("Event %s has no parameter called %s" % (self.event_name, k)) | |||
setattr(self, k, v) | |||
unset.remove(k) | |||
for k in unset: | |||
setattr(self, k, None) | |||
self._delivered_to = set() | |||
def __repr__(self): | |||
kvs = {'name': self.event_name} | |||
for k in self._props_sorted: | |||
kvs[k] = getattr(self, k) | |||
return '<%s %s>' % (self.__class__.__name__, ' '.join(['%s=%s' % kv for kv in kvs.items()]),) | |||
def _params(self): | |||
return {k: getattr(self, k) for k in self._props} | |||
@classmethod | |||
def _handler_method_name(cls): | |||
name = 'recv_' + _uncamelcase(cls.event_name) | |||
if cls._internal: | |||
name = '_' + name | |||
return name | |||
def _dispatch_to_func(self, f): | |||
return f(self, **self._params()) | |||
def _dispatch_to_obj(self, obj, fallback_to_default=True): | |||
for cls in self._parent_event_classes(): | |||
f = getattr(obj, cls._handler_method_name(), None) | |||
if f and not self._is_filtered(f): | |||
return self._dispatch_to_func(f) | |||
if fallback_to_default: | |||
name = 'recv_default_handler' | |||
if self._internal: | |||
name = '_' + name | |||
f = getattr(obj, name, None) | |||
if f and not self._is_filtered(f): | |||
return f(self, **self._params()) | |||
def _dispatch_to_future(self, fut): | |||
if not fut.done(): | |||
fut.set_result(self) | |||
def _is_filtered(self, f): | |||
filters = getattr(f, '_handler_filters', None) | |||
if filters is None: | |||
return False | |||
for filter in filters: | |||
if filter(self): | |||
return False | |||
return True | |||
def _parent_event_classes(self): | |||
for cls in self.__class__.__mro__: | |||
if cls != Event and issubclass(cls, Event): | |||
yield cls | |||
def _register_dynamic_event_type(event_name, attrs): | |||
return type(event_name, (Event,), attrs) | |||
class Handler(collections.namedtuple('Handler', 'obj evt f')): | |||
'''A Handler is returned by :meth:`Dispatcher.add_event_handler` | |||
The handler can be disabled at any time by calling its :meth:`disable` | |||
method. | |||
''' | |||
__slots__ = () | |||
def disable(self): | |||
'''Removes the handler from the object it was originally registered with.''' | |||
return self.obj.remove_event_handler(self.evt, self.f) | |||
@property | |||
def oneshot(self): | |||
'''bool: True if the wrapped handler function will only be called once.''' | |||
return getattr(self.f, '_oneshot_handler', False) | |||
class NullHandler(Handler): | |||
def disable(self): | |||
pass | |||
class Dispatcher(base.Base): | |||
'''Mixin to provide event dispatch handling.''' | |||
def __init__(self, *a, dispatch_parent=None, loop=None, **kw): | |||
super().__init__(**kw) | |||
active_dispatchers.add(self) | |||
self._dispatch_parent = dispatch_parent | |||
self._dispatch_children = [] | |||
self._dispatch_handlers = collections.defaultdict(list) | |||
if not loop: | |||
raise ValueError("Loop was not supplied to "+self.__class__.__name__) | |||
self._loop = loop or asyncio.get_event_loop() | |||
self._dispatcher_running = True | |||
def _set_parent_dispatcher(self, parent): | |||
self._dispatch_parent = parent | |||
def _add_child_dispatcher(self, child): | |||
self._dispatch_children.append(child) | |||
def _stop_dispatcher(self): | |||
"""Stop dispatching events - call before closing the connection to prevent stray dispatched events""" | |||
self._dispatcher_running = False | |||
def add_event_handler(self, event, f): | |||
"""Register an event handler to be notified when this object receives a type of Event. | |||
Expects a subclass of Event as the first argument. If the class has | |||
subclasses then the handler will be notified for events of that subclass too. | |||
For example, adding a handler for :class:`~cozmo.action.EvtActionCompleted` | |||
will cause the handler to also be notified for | |||
:class:`~cozmo.anim.EvtAnimationCompleted` as it's a subclass. | |||
Callable handlers (e.g. functions) are called with a first argument | |||
containing an Event instance and the remaining keyword arguments set as | |||
the event parameters. | |||
For example, ``def my_ontap_handler(evt, *, obj, tap_count, **kwargs)`` | |||
or ``def my_ontap_handler(evt, obj=None, tap_count=None, **kwargs)`` | |||
It's recommended that a ``**kwargs`` parameter be included in the | |||
definition so that future expansion of event parameters do not cause | |||
the handler to fail. | |||
Callable handlers may raise an events.StopProgation exception to prevent | |||
other handlers listening to the same event from being triggered. | |||
:class:`asyncio.Future` handlers are called with a result set to the event. | |||
Args: | |||
event (:class:`Event`): A subclass of :class:`Event` (not an instance of that class) | |||
f (callable): A callable or :class:`asyncio.Future` to execute when the event is received | |||
Raises: | |||
:class:`TypeError`: An invalid event type was supplied | |||
""" | |||
if not issubclass(event, Event): | |||
raise TypeError("event must be a subclass of Event (not an instance)") | |||
if not self._dispatcher_running: | |||
return NullHandler(self, event, f) | |||
if isinstance(f, asyncio.Future): | |||
# futures can only be called once. | |||
f = oneshot(f) | |||
handler = Handler(self, event, f) | |||
self._dispatch_handlers[event.event_name].append(handler) | |||
return handler | |||
def remove_event_handler(self, event, f): | |||
"""Remove an event handler for this object. | |||
Args: | |||
event (:class:`Event`): The event class, or an instance thereof, | |||
used with register_event_handler. | |||
f (callable or :class:`Handler`): The callable object that was | |||
passed as a handler to :meth:`add_event_handler`, or a | |||
:class:`Handler` instance that was returned by | |||
:meth:`add_event_handler`. | |||
Raises: | |||
:class:`ValueError`: No matching handler found. | |||
""" | |||
if not (isinstance(event, Event) or (isinstance(event, type) and issubclass(event, Event))): | |||
raise TypeError("event must be a subclasss or instance of Event") | |||
if isinstance(f, Handler): | |||
for i, h in enumerate(self._dispatch_handlers[event.event_name]): | |||
if h == f: | |||
del self._dispatch_handlers[event.event_name][i] | |||
return | |||
else: | |||
for i, h in enumerate(self._dispatch_handlers[event.event_name]): | |||
if h.f == f: | |||
del self._dispatch_handlers[event.event_name][i] | |||
return | |||
raise ValueError("No matching handler found for %s (%s)" % (event.event_name, f) ) | |||
def dispatch_event(self, event, **kw): | |||
'''Dispatches a single event to registered handlers. | |||
Not generally called from user-facing code. | |||
Args: | |||
event (:class:`Event`): An class or instance of :class:`Event` | |||
kw (dict): If a class is passed to event, then the remaining keywords | |||
are passed to it to create an instance of the event. | |||
Returns: | |||
A :class:`asyncio.Task` or :class:`asyncio.Future` that will | |||
complete once all event handlers have been called. | |||
Raises: | |||
:class:`TypeError` if an invalid event is supplied. | |||
''' | |||
if not self._dispatcher_running: | |||
return | |||
event_cls = event | |||
if not isinstance(event, Event): | |||
if not isinstance(event, type) or not issubclass(event, Event): | |||
raise TypeError("events must be a subclass or instance of Event") | |||
# create an instance of the event if passed a class | |||
event = event(**kw) | |||
else: | |||
event_cls = event.__class__ | |||
if id(self) in event._delivered_to: | |||
return | |||
event._delivered_to.add(id(self)) | |||
handlers = set() | |||
for cls in event._parent_event_classes(): | |||
for handler in self._dispatch_handlers[cls.event_name]: | |||
if event._is_filtered(handler.f): | |||
continue | |||
if getattr(handler.f, '_oneshot_handler', False): | |||
# Disable oneshot events prior to actual dispatch | |||
handler.disable() | |||
handlers.add(handler) | |||
return asyncio.ensure_future(self._dispatch_event(event, handlers), loop=self._loop) | |||
async def _dispatch_event(self, event, handlers): | |||
# iterate through events from child->parent | |||
# update the dispatched_to set for each event so each handler | |||
# only receives the most specific event if they are monitoring for both. | |||
try: | |||
# dispatch to local handlers | |||
for handler in handlers: | |||
if isinstance(handler.f, asyncio.Future): | |||
event._dispatch_to_future(handler.f) | |||
else: | |||
result = event._dispatch_to_func(handler.f) | |||
if asyncio.iscoroutine(result): | |||
await result | |||
# dispatch to children | |||
for child in self._dispatch_children: | |||
child.dispatch_event(event) | |||
# dispatch to self methods | |||
result = event._dispatch_to_obj(self) | |||
if asyncio.iscoroutine(result): | |||
await result | |||
# dispatch to parent dispatcher | |||
if self._dispatch_parent: | |||
self._dispatch_parent.dispatch_event(event) | |||
except exceptions.StopPropogation: | |||
pass | |||
def _abort_event_futures(self, exc): | |||
'''Sets an exception on all pending Future handlers | |||
This prevents coroutines awaiting a Future from blocking forever | |||
should a hard failure occur with the connection. | |||
''' | |||
handlers = set() | |||
for evh in self._dispatch_handlers.values(): | |||
for h in evh: | |||
handlers.add(h) | |||
for handler in handlers: | |||
if isinstance(handler.f, asyncio.Future): | |||
if not handler.f.done(): | |||
handler.f.set_exception(exc) | |||
handler.disable() | |||
async def wait_for(self, event_or_filter, timeout=30): | |||
'''Waits for the specified event to be sent to the current object. | |||
Args: | |||
event_or_filter (:class:`Event`): Either a :class:`Event` class | |||
or a :class:`Filter` instance to wait to trigger | |||
timeout: Maximum time to wait for the event. Pass None to wait indefinitely. | |||
Returns: | |||
The :class:`Event` instance that was dispatched | |||
Raises: | |||
:class:`asyncio.TimeoutError` | |||
''' | |||
f = asyncio.Future(loop=self._loop) # replace with loop.create_future in 3.5.2 | |||
# TODO: add a timer that logs every 5 seconds that the event is still being | |||
# waited on. Will help novice programmers realize why their program is hanging. | |||
f = oneshot(f) | |||
if isinstance(event_or_filter, Filter): | |||
f = filter_handler(event_or_filter)(f) | |||
event = event_or_filter._event | |||
else: | |||
event = event_or_filter | |||
self.add_event_handler(event, f) | |||
if timeout: | |||
return await asyncio.wait_for(f, timeout, loop=self._loop) | |||
return await f | |||
def oneshot(f): | |||
'''Event handler decorator; causes the handler to only be dispatched to once.''' | |||
f._oneshot_handler = True | |||
return f | |||
def filter_handler(event, **filters): | |||
'''Decorates a handler function or Future to only be called if a filter is matched. | |||
A handler may apply multiple separate filters; the handlers will be called | |||
if any of those filters matches. | |||
For example:: | |||
# Handle only if the anim_majorwin animation completed | |||
@filter_handler(cozmo.anim.EvtAnimationCompleted, animation_name="anim_majorwin") | |||
# Handle only when the observed object is a LightCube | |||
@filter_handler(cozmo.objects.EvtObjectObserved, obj=lambda obj: isinstance(cozmo.objects.LightCube)) | |||
Args: | |||
event (:class:`Event`): The event class to match on | |||
filters (dict): Zero or more event parameters to filter on. Values may | |||
be either strings for exact matches, or functions which accept the | |||
value as the first argument and return a bool indicating whether | |||
the value passes the filter. | |||
''' | |||
if isinstance(event, Filter): | |||
if len(filters) != 0: | |||
raise ValueError("Cannot supply filter values when passing a Filter as the first argument") | |||
filter = event | |||
else: | |||
filter = Filter(event, **filters) | |||
def filter_property(f): | |||
if hasattr(f, '_handler_filters'): | |||
f._handler_filters.append(filter) | |||
else: | |||
f._handler_filters = [filter] | |||
return f | |||
return filter_property | |||
class Filter: | |||
"""Provides fine-grain filtering of events for dispatch. | |||
See the ::func::`filter_handler` method for further details. | |||
""" | |||
def __init__(self, event, **filters): | |||
if not issubclass(event, Event): | |||
raise TypeError("event must be a subclass of Event (not an instance)") | |||
self._event = event | |||
self._filters = filters | |||
for key in self._filters.keys(): | |||
if not hasattr(event, key): | |||
raise AttributeError("Event %s does not define property %s", event.__name__, key) | |||
def __setattr__(self, key, val): | |||
if key[0] == '_': | |||
return super().__setattr__(key, val) | |||
if not hasattr(self._event, key): | |||
raise AttributeError("Event %s does not define property %s", self._event.__name__, key) | |||
self._filters[key] = val | |||
def __call__(self, evt): | |||
for prop, filter in self._filters.items(): | |||
val = getattr(evt, prop) | |||
if callable(filter): | |||
if not filter(val): | |||
return False | |||
elif val != filter: | |||
return False | |||
return True | |||
async def wait_for_first(*futures, discard_remaining=True, loop=None): | |||
'''Wait the first of a set of futures to complete. | |||
Eg:: | |||
event = cozmo.event.wait_for_first( | |||
coz.world.wait_for_new_cube(), | |||
playing_anim.wait_for(cozmo.anim.EvtAnimationCompleted) | |||
) | |||
If more than one completes during a single event loop run, then | |||
if any of those results are not exception, one of them will be selected | |||
(at random, as determined by ``set.pop``) to be returned, else one | |||
of the result exceptions will be raised instead. | |||
Args: | |||
futures (list of :class:`asyncio.Future`): The futures or coroutines to wait on. | |||
discard_remaining (bool): Cancel or discard the results of the futures | |||
that did not return first. | |||
loop (:class:`asyncio.BaseEventLoop`): The event loop to wait on. | |||
Returns: | |||
The first result, or raised exception | |||
''' | |||
done, pending = await asyncio.wait(futures, loop=loop, return_when=asyncio.FIRST_COMPLETED) | |||
# collect the results from all "done" futures; only one will be returned | |||
result = None | |||
for fut in done: | |||
try: | |||
fut_result = fut.result() | |||
if result is None or isinstance(result, BaseException): | |||
result = fut_result | |||
except Exception as exc: | |||
if result is None: | |||
result = exc | |||
if discard_remaining: | |||
# cancel the pending futures | |||
for fut in pending: | |||
fut.cancel() | |||
if isinstance(result, BaseException): | |||
raise result | |||
return result | |||
def _abort_futures(exc): | |||
'''Trigger the exception handler for all pending Future handlers.''' | |||
for obj in active_dispatchers: | |||
obj._abort_event_futures(exc) |
@@ -0,0 +1,74 @@ | |||
# Copyright (c) 2016 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. | |||
'''SDK-specific exception classes.''' | |||
# __all__ should order by constants, event classes, other classes, functions. | |||
__all__ = ['CozmoSDKException', 'SDKShutdown', 'StopPropogation', | |||
'AnimationsNotLoaded', 'ActionError', 'ConnectionError', | |||
'ConnectionAborted', 'ConnectionCheckFailed', 'NoDevicesFound', | |||
'SDKVersionMismatch', 'NotPickupable', 'CannotPlaceObjectsOnThis', | |||
'RobotBusy', 'InvalidOpenGLGlutImplementation'] | |||
class CozmoSDKException(Exception): | |||
'''Base class of all Cozmo SDK exceptions.''' | |||
class SDKShutdown(CozmoSDKException): | |||
'''Raised when the SDK is being shut down''' | |||
class StopPropogation(CozmoSDKException): | |||
'''Raised by event handlers to prevent further handlers from being triggered.''' | |||
class AnimationsNotLoaded(CozmoSDKException): | |||
'''Raised if an attempt is made to play a named animation before animations have been received.''' | |||
class ActionError(CozmoSDKException): | |||
'''Base class for errors that occur with robot actions.''' | |||
class ConnectionError(CozmoSDKException): | |||
'''Base class for errors regarding connection to the device.''' | |||
class ConnectionAborted(ConnectionError): | |||
'''Raised if the connection to the device is unexpectedly lost.''' | |||
class ConnectionCheckFailed(ConnectionError): | |||
'''Raised if the connection check has failed.''' | |||
class NoDevicesFound(ConnectionError): | |||
'''Raised if no devices connected running Cozmo in SDK mode''' | |||
class SDKVersionMismatch(ConnectionError): | |||
'''Raised if the Cozmo SDK version is not compatible with the software running on the device.''' | |||
def __init__(self, message, sdk_version, sdk_app_version, app_version, *args): | |||
super().__init__(message, sdk_version, sdk_app_version, app_version, *args) | |||
#: str: The SDK version number in Major.Minor.Patch format. | |||
#: See :ref:`sdk-versions` for which App version is compatible with each SDK version. | |||
self.sdk_version = sdk_version | |||
#: str: The version of the App that this SDK is compatible with in Major.Minor.Patch format. | |||
self.sdk_app_version = sdk_app_version | |||
#: str: The version of the App that was detected, and is incompatible, in Major.Minor.Patch format. | |||
self.app_version = app_version | |||
class NotPickupable(ActionError): | |||
'''Raised if an attempt is made to pick up or place an object that can't be picked up by Cozmo''' | |||
class CannotPlaceObjectsOnThis(ActionError): | |||
'''Raised if an attempt is made to place an object on top of an invalid object''' | |||
class RobotBusy(ActionError): | |||
'''Raised if an attempt is made to perform an action while another action is still running.''' | |||
class InvalidOpenGLGlutImplementation(ImportError): | |||
'''Raised by opengl viewer if no valid GLUT implementation available.''' |
@@ -0,0 +1,445 @@ | |||
# 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. | |||
'''Face recognition and enrollment. | |||
Cozmo is capable of recognizing human faces, tracking their position and rotation | |||
("pose") and assigning names to them via an enrollment process. | |||
The :class:`cozmo.world.World` object keeps track of faces the robot currently | |||
knows about, along with those that are currently visible to the camera. | |||
Each face is assigned a :class:`Face` object, which generates a number of | |||
observable events whenever the face is observed, has its ID updated, is | |||
renamed, etc. | |||
Note that these face-specific events are also passed up to the | |||
:class:`cozmo.world.World` object, so events for all known faces can be | |||
observed by adding handlers there. | |||
''' | |||
# __all__ should order by constants, event classes, other classes, functions. | |||
__all__ = ['FACE_VISIBILITY_TIMEOUT', | |||
'FACIAL_EXPRESSION_UNKNOWN', 'FACIAL_EXPRESSION_NEUTRAL', 'FACIAL_EXPRESSION_HAPPY', | |||
'FACIAL_EXPRESSION_SURPRISED', 'FACIAL_EXPRESSION_ANGRY', 'FACIAL_EXPRESSION_SAD', | |||
'EvtErasedEnrolledFace', 'EvtFaceAppeared', 'EvtFaceDisappeared', | |||
'EvtFaceIdChanged', 'EvtFaceObserved', 'EvtFaceRenamed', | |||
'Face', | |||
'erase_all_enrolled_faces', 'erase_enrolled_face_by_id', | |||
'update_enrolled_face_by_id'] | |||
from . import logger | |||
from . import behavior | |||
from . import event | |||
from . import objects | |||
from . import util | |||
from ._clad import _clad_to_engine_iface | |||
from ._clad import _clad_to_game_anki | |||
#: Length of time in seconds to go without receiving an observed event before | |||
#: assuming that Cozmo can no longer see a face. | |||
FACE_VISIBILITY_TIMEOUT = objects.OBJECT_VISIBILITY_TIMEOUT | |||
# Facial expressions that Cozmo can distinguish | |||
#: Facial expression not recognized. | |||
#: Call :func:`cozmo.robot.Robot.enable_facial_expression_estimation` to enable recognition. | |||
FACIAL_EXPRESSION_UNKNOWN = "unknown" | |||
#: Facial expression neutral | |||
FACIAL_EXPRESSION_NEUTRAL = "neutral" | |||
#: Facial expression happy | |||
FACIAL_EXPRESSION_HAPPY = "happy" | |||
#: Facial expression surprised | |||
FACIAL_EXPRESSION_SURPRISED = "surprised" | |||
#: Facial expression angry | |||
FACIAL_EXPRESSION_ANGRY = "angry" | |||
#: Facial expression sad | |||
FACIAL_EXPRESSION_SAD = "sad" | |||
class EvtErasedEnrolledFace(event.Event): | |||
'''Triggered when a face enrollment is removed (via erase_enrolled_face_by_id)''' | |||
face = 'The Face instance that the enrollment is being erased for' | |||
old_name = 'The name previously used for this face' | |||
class EvtFaceIdChanged(event.Event): | |||
'''Triggered whenever a face has its ID updated in engine. | |||
Generally occurs when: | |||
1) A tracked but unrecognized face (negative ID) is recognized and receives a positive ID or | |||
2) Face records get merged (on realization that 2 faces are actually the same) | |||
''' | |||
face = 'The Face instance that is being given a new id' | |||
old_id = 'The ID previously used for this face' | |||
new_id = 'The new ID that will be used for this face' | |||
class EvtFaceObserved(event.Event): | |||
'''Triggered whenever a face is visually identified by the robot. | |||
A stream of these events are produced while a face is visible to the robot. | |||
Each event has an updated image_box field. | |||
See EvtFaceAppeared if you only want to know when a face first | |||
becomes visible. | |||
''' | |||
face = 'The Face instance that was observed' | |||
updated = 'A set of field names that have changed' | |||
image_box = 'A comzo.util.ImageBox defining where the face is within Cozmo\'s camera view' | |||
name = 'The name associated with the face that was observed' | |||
pose = 'The cozmo.util.Pose defining the position and rotation of the face.' | |||
class EvtFaceAppeared(event.Event): | |||
'''Triggered whenever a face is first visually identified by a robot. | |||
This differs from EvtFaceObserved in that it's only triggered when | |||
a face initially becomes visible. If it disappears for more than | |||
FACE_VISIBILITY_TIMEOUT seconds and then is seen again, a | |||
EvtFaceDisappeared will be dispatched, followed by another | |||
EvtFaceAppeared event. | |||
For continuous tracking information about a visible face, see | |||
EvtFaceObserved. | |||
''' | |||
face = 'The Face instance that was observed' | |||
updated = 'A set of field names that have changed' | |||
image_box = 'A comzo.util.ImageBox defining where the face is within Cozmo\'s camera view' | |||
name = 'The name associated with the face that was observed' | |||
pose = 'The cozmo.util.Pose defining the position and rotation of the face.' | |||
class EvtFaceDisappeared(event.Event): | |||
'''Triggered whenever a face that was previously being observed is no longer visible.''' | |||
face = 'The Face instance that is no longer being observed' | |||
class EvtFaceRenamed(event.Event): | |||
'''Triggered whenever a face is renamed (via RobotRenamedEnrolledFace)''' | |||
face = 'The Face instance that is being given a new name' | |||
old_name = 'The name previously used for this face' | |||
new_name = 'The new name that will be used for this face' | |||
def erase_all_enrolled_faces(conn): | |||
'''Erase the enrollment (name) records for all faces. | |||
Args: | |||
conn (:class:`~cozmo.conn.CozmoConnection`): The connection to send the message over | |||
''' | |||
msg = _clad_to_engine_iface.EraseAllEnrolledFaces() | |||
conn.send_msg(msg) | |||
def erase_enrolled_face_by_id(conn, face_id): | |||
'''Erase the enrollment (name) record for the face with this ID. | |||
Args: | |||
conn (:class:`~cozmo.conn.CozmoConnection`): The connection to send the message over | |||
face_id (int): The ID of the face to erase. | |||
''' | |||
msg = _clad_to_engine_iface.EraseEnrolledFaceByID(face_id) | |||
conn.send_msg(msg) | |||
def update_enrolled_face_by_id(conn, face_id, old_name, new_name): | |||
'''Update the name enrolled for a given face. | |||
Args: | |||
conn (:class:`~cozmo.conn.CozmoConnection`): The connection to send the message over. | |||
face_id (int): The ID of the face to rename. | |||
old_name (string): The old name of the face (must be correct, otherwise message is ignored). | |||
new_name (string): The new name for the face. | |||
''' | |||
msg = _clad_to_engine_iface.UpdateEnrolledFaceByID(face_id, old_name, new_name) | |||
conn.send_msg(msg) | |||
def _clad_facial_expression_to_facial_expression(clad_expression_type): | |||
if clad_expression_type == _clad_to_game_anki.Vision.FacialExpression.Unknown: | |||
return FACIAL_EXPRESSION_UNKNOWN | |||
elif clad_expression_type == _clad_to_game_anki.Vision.FacialExpression.Neutral: | |||
return FACIAL_EXPRESSION_NEUTRAL | |||
elif clad_expression_type == _clad_to_game_anki.Vision.FacialExpression.Happiness: | |||
return FACIAL_EXPRESSION_HAPPY | |||
elif clad_expression_type == _clad_to_game_anki.Vision.FacialExpression.Surprise: | |||
return FACIAL_EXPRESSION_SURPRISED | |||
elif clad_expression_type == _clad_to_game_anki.Vision.FacialExpression.Anger: | |||
return FACIAL_EXPRESSION_ANGRY | |||
elif clad_expression_type == _clad_to_game_anki.Vision.FacialExpression.Sadness: | |||
return FACIAL_EXPRESSION_SAD | |||
else: | |||
raise ValueError("Unexpected facial expression type %s" % clad_expression_type) | |||
class Face(objects.ObservableElement): | |||
'''A single face that Cozmo has detected. | |||
May represent a face that has previously been enrolled, in which case | |||
:attr:`name` will hold the name that it was enrolled with. | |||
Each Face instance has a :attr:`face_id` integer - This may change if | |||
Cozmo later gets an improved view and makes a different prediction about | |||
which face it is looking at. | |||
See parent class :class:`~cozmo.objects.ObservableElement` for additional properties | |||
and methods. | |||
''' | |||
#: Length of time in seconds to go without receiving an observed event before | |||
#: assuming that Cozmo can no longer see a face. | |||
visibility_timeout = FACE_VISIBILITY_TIMEOUT | |||
def __init__(self, conn, world, robot, face_id=None, **kw): | |||
super().__init__(conn, world, robot, **kw) | |||
self._face_id = face_id | |||
self._updated_face_id = None | |||
self._name = '' | |||
self._expression = None | |||
self._expression_score = None | |||
self._left_eye = None | |||
self._right_eye = None | |||
self._nose = None | |||
self._mouth = None | |||
def _repr_values(self): | |||
return 'face_id=%s,%s name=%s' % (self.face_id, self.updated_face_id, | |||
self.name) | |||
#### Private Methods #### | |||
def _dispatch_observed_event(self, changed_fields, image_box): | |||
self.dispatch_event(EvtFaceObserved, face=self, name=self._name, | |||
updated=changed_fields, image_box=image_box, pose=self._pose) | |||
def _dispatch_appeared_event(self, changed_fields, image_box): | |||
self.dispatch_event(EvtFaceAppeared, face=self, | |||
updated=changed_fields, image_box=image_box, pose=self._pose) | |||
def _dispatch_disappeared_event(self): | |||
self.dispatch_event(EvtFaceDisappeared, face=self) | |||
#### Properties #### | |||
@property | |||
def face_id(self): | |||
'''int: The internal ID assigned to the face. | |||
This value can only be assigned once as it is static in the engine. | |||
''' | |||
return self._face_id | |||
@face_id.setter | |||
def face_id(self, value): | |||
if self._face_id is not None: | |||
raise ValueError("Cannot change face ID once set (from %s to %s)" % (self._face_id, value)) | |||
logger.debug("Updated face_id for %s from %s to %s", self.__class__, self._face_id, value) | |||
self._face_id = value | |||
@property | |||
def has_updated_face_id(self): | |||
'''bool: True if this face been updated / superseded by a face with a new ID''' | |||
return self._updated_face_id is not None | |||
@property | |||
def updated_face_id(self): | |||
'''int: The ID for the face that superseded this one (if any, otherwise :meth:`face_id`)''' | |||
if self.has_updated_face_id: | |||
return self._updated_face_id | |||
else: | |||
return self.face_id | |||
@property | |||
def name(self): | |||
'''string: The name Cozmo has associated with the face in his memory. | |||
This string will be empty if the face is not recognized or enrolled. | |||
''' | |||
return self._name | |||
@property | |||
def expression(self): | |||
'''string: The facial expression Cozmo has recognized on the face. | |||
Will be :attr:`FACIAL_EXPRESSION_UNKNOWN` by default if you haven't called | |||
:meth:`cozmo.robot.Robot.enable_facial_expression_estimation` to enable | |||
the facial expression estimation. Otherwise it will be equal to one of: | |||
:attr:`FACIAL_EXPRESSION_NEUTRAL`, :attr:`FACIAL_EXPRESSION_HAPPY`, | |||
:attr:`FACIAL_EXPRESSION_SURPRISED`, :attr:`FACIAL_EXPRESSION_ANGRY`, | |||
or :attr:`FACIAL_EXPRESSION_SAD`. | |||
''' | |||
return self._expression | |||
@property | |||
def expression_score(self): | |||
'''int: The score/confidence that :attr:`expression` was correct. | |||
Will be 0 if expression is :attr:`FACIAL_EXPRESSION_UNKNOWN` (e.g. if | |||
:meth:`cozmo.robot.Robot.enable_facial_expression_estimation` wasn't | |||
called yet). The maximum possible score is 100. | |||
''' | |||
return self._expression_score | |||
@property | |||
def known_expression(self): | |||
'''string: The known facial expression Cozmo has recognized on the face. | |||
Like :meth:`expression` but returns an empty string for the unknown expression. | |||
''' | |||
expression = self.expression | |||
if expression == FACIAL_EXPRESSION_UNKNOWN: | |||
return "" | |||
return expression | |||
@property | |||
def left_eye(self): | |||
'''sequence of tuples of float (x,y): points representing the outline of the left eye''' | |||
return self._left_eye | |||
@property | |||
def right_eye(self): | |||
'''sequence of tuples of float (x,y): points representing the outline of the right eye''' | |||
return self._right_eye | |||
@property | |||
def nose(self): | |||
'''sequence of tuples of float (x,y): points representing the outline of the nose''' | |||
return self._nose | |||
@property | |||
def mouth(self): | |||
'''sequence of tuples of float (x,y): points representing the outline of the mouth''' | |||
return self._mouth | |||
#### Private Event Handlers #### | |||
def _recv_msg_robot_observed_face(self, evt, *, msg): | |||
changed_fields = {'pose', 'left_eye', 'right_eye', 'nose', 'mouth'} | |||
self._pose = util.Pose._create_from_clad(msg.pose) | |||
self._name = msg.name | |||
expression = _clad_facial_expression_to_facial_expression(msg.expression) | |||
expression_score = 0 | |||
if expression != FACIAL_EXPRESSION_UNKNOWN: | |||
expression_score = msg.expressionValues[msg.expression] | |||
if expression_score == 0: | |||
# The expression should have been marked unknown - this is a | |||
# bug in the engine because even a zero score overwrites the | |||
# default negative score for Unknown. | |||
expression = FACIAL_EXPRESSION_UNKNOWN | |||
if expression != self._expression: | |||
self._expression = expression | |||
changed_fields.add('expression') | |||
if expression_score != self._expression_score: | |||
self._expression_score = expression_score | |||
changed_fields.add('expression_score') | |||
self._left_eye = msg.leftEye | |||
self._right_eye = msg.rightEye | |||
self._nose = msg.nose | |||
self._mouth = msg.mouth | |||
image_box = util.ImageBox._create_from_clad_rect(msg.img_rect) | |||
self._on_observed(image_box, msg.timestamp, changed_fields) | |||
def _recv_msg_robot_changed_observed_face_id(self, evt, *, msg): | |||
self._updated_face_id = msg.newID | |||
self.dispatch_event(EvtFaceIdChanged, face=self, old_id=msg.oldID, new_id = msg.newID) | |||
def _recv_msg_robot_renamed_enrolled_face(self, evt, *, msg): | |||
old_name = self._name | |||
self._name = msg.name | |||
self.dispatch_event(EvtFaceRenamed, face=self, old_name=old_name, new_name=msg.name) | |||
def _recv_msg_robot_erased_enrolled_face(self, evt, *, msg): | |||
old_name = self._name | |||
self._name = '' | |||
self.dispatch_event(EvtErasedEnrolledFace, face=self, old_name=old_name) | |||
#### Public Event Handlers #### | |||
#### Event Wrappers #### | |||
#### Commands #### | |||
def _is_valid_name(self, name): | |||
if not (name and name.isalpha()): | |||
return False | |||
try: | |||
name.encode('ascii') | |||
except UnicodeEncodeError: | |||
return False | |||
return True | |||
def name_face(self, name): | |||
'''Assign a name to this face. Cozmo will remember this name between SDK runs. | |||
Args: | |||
name (string): The name that will be assigned to this face. Must | |||
be a non-empty ASCII string of alphabetic characters only. | |||
Returns: | |||
An instance of :class:`cozmo.behavior.Behavior` object | |||
Raises: | |||
:class:`ValueError` if name is invalid. | |||
''' | |||
if not self._is_valid_name(name): | |||
raise ValueError("new_name '%s' is an invalid face name. " | |||
"Must be non-empty and contain only alphabetic ASCII characters." % name) | |||
logger.info("Enrolling face=%s with name='%s'", self, name) | |||
# Note: saveID must be 0 if face_id doesn't already have a name | |||
msg = _clad_to_engine_iface.SetFaceToEnroll(name=name, | |||
observedID=self.face_id, | |||
saveID=0, | |||
saveToRobot=True, | |||
sayName=False, | |||
useMusic=False) | |||
self.conn.send_msg(msg) | |||
enroll_behavior = self._robot.start_behavior(behavior.BehaviorTypes._EnrollFace) | |||
return enroll_behavior | |||
def rename_face(self, new_name): | |||
'''Change the name assigned to the face. Cozmo will remember this name between SDK runs. | |||
Args: | |||
new_name (string): The new name that will be assigned to this face. Must | |||
be a non-empty ASCII string of alphabetic characters only. | |||
Raises: | |||
:class:`ValueError` if new_name is invalid. | |||
''' | |||
if not self._is_valid_name(new_name): | |||
raise ValueError("new_name '%s' is an invalid face name. " | |||
"Must be non-empty and contain only alphabetic ASCII characters." % new_name) | |||
update_enrolled_face_by_id(self.conn, self.face_id, self.name, new_name) | |||
def erase_enrolled_face(self): | |||
'''Remove the name associated with this face. | |||
Cozmo will no longer remember the name associated with this face between SDK runs. | |||
''' | |||
erase_enrolled_face_by_id(self.conn, self.face_id) | |||
@@ -0,0 +1,195 @@ | |||
# Copyright (c) 2016 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. | |||
'''Helper routines for dealing with Cozmo's lights and colors.''' | |||
# __all__ should order by constants, event classes, other classes, functions. | |||
__all__ = ['green', 'red', 'blue', 'white', 'off', | |||
'green_light', 'red_light', 'blue_light', 'white_light', 'off_light', | |||
'Color', 'Light'] | |||
import copy | |||
from . import logger | |||
class Color: | |||
'''A Color to be used with a Light. | |||
Either int_color or rgb may be used to specify the actual color. | |||
Any alpha components (from int_color) are ignored - all colors are fully opaque. | |||
Args: | |||
int_color (int): A 32 bit value holding the binary RGBA value (where A | |||
is ignored and forced to be fully opaque). | |||
rgb (tuple): A tuple holding the integer values from 0-255 for (red, green, blue) | |||
name (str): A name to assign to this color | |||
''' | |||
def __init__(self, int_color=None, rgb=None, name=None): | |||
self.name = name | |||
self._int_color = 0 | |||
if int_color is not None: | |||
self._int_color = int_color | 0xff | |||
elif rgb is not None: | |||
self._int_color = (rgb[0] << 24) | (rgb[1] << 16) | (rgb[2] << 8) | 0xff | |||
@property | |||
def int_color(self): | |||
'''int: The encoded integer value of the color.''' | |||
return self._int_color | |||
#: :class:`Color`: Green color instance. | |||
green = Color(name="green", int_color=0x00ff00ff) | |||
#: :class:`Color`: Red color instance. | |||
red = Color(name="red", int_color=0xff0000ff) | |||
#: :class:`Color`: Blue color instance. | |||
blue = Color(name="blue", int_color=0x0000ffff) | |||
#: :class:`Color`: White color instance. | |||
white = Color(name="white", int_color=0xffffffff) | |||
#: :class:`Color`: instance representing no color (LEDs off). | |||
off = Color(name="off") | |||
class Light: | |||
'''Lights are used with LightCubes and Cozmo's backpack. | |||
Lights may either be "on" or "off", though in practice any colors may be | |||
assigned to either state (including no color/light). | |||
''' | |||
def __init__(self, on_color=off, off_color=off, on_period_ms=250, | |||
off_period_ms=0, transition_on_period_ms=0, transition_off_period_ms=0): | |||
self._on_color = on_color | |||
self._off_color = off_color | |||
self._on_period_ms = on_period_ms | |||
self._off_period_ms = off_period_ms | |||
self._transition_on_period_ms = transition_on_period_ms | |||
self._transition_off_period_ms = transition_off_period_ms | |||
@property | |||
def on_color(self): | |||
''':class:`Color`: The Color shown when the light is on.''' | |||
return self._on_color | |||
@on_color.setter | |||
def on_color(self, color): | |||
if not isinstance(color, Color): | |||
raise TypeError("Must specify a Color") | |||
self._on_color = color | |||
@property | |||
def off_color(self): | |||
''':class:`Color`: The Color shown when the light is off.''' | |||
return self._off_color | |||
@off_color.setter | |||
def off_color(self, color): | |||
if not isinstance(color, Color): | |||
raise TypeError("Must specify a Color") | |||
self._off_color = color | |||
@property | |||
def on_period_ms(self): | |||
'''int: The number of milliseconds the light should be "on" for for each cycle.''' | |||
return self._on_period_ms | |||
@on_period_ms.setter | |||
def on_period_ms(self, ms): | |||
if not 0 < ms < 2**32: | |||
raise ValueError("Invalid value") | |||
self._on_period_ms = ms | |||
@property | |||
def off_period_ms(self): | |||
'''int: The number of milliseconds the light should be "off" for for each cycle.''' | |||
return self._off_period_ms | |||
@off_period_ms.setter | |||
def off_period_ms(self, ms): | |||
if not 0 < ms < 2**32: | |||
raise ValueError("Invalid value") | |||
self._off_period_ms = ms | |||
@property | |||
def transition_on_period_ms(self): | |||
'''int: The number of milliseconds to take to transition the light to the on color.''' | |||
return self._transition_on_period_ms | |||
@transition_on_period_ms.setter | |||
def transition_on_period_ms(self, ms): | |||
if not 0 < ms < 2**32: | |||
raise ValueError("Invalid value") | |||
self._transition_on_period_ms = ms | |||
@property | |||
def transition_off_period_ms(self): | |||
'''int: The number of milliseconds to take to transition the light to the off color.''' | |||
return self._transition_off_period_ms | |||
@transition_off_period_ms.setter | |||
def transition_off_period_ms(self, ms): | |||
if not 0 < ms < 2**32: | |||
raise ValueError("Invalid value") | |||
self._transition_off_period_ms = ms | |||
def flash(self, on_period_ms=250, off_period_ms=250, off_color=off): | |||
'''Convenience function to make a flashing version of an existing Light instance. | |||
Args: | |||
on_period_ms (int): The number of milliseconds the light should be "on" for for each cycle. | |||
off_period_ms (int): The number of milliseconds the light should be "off" for for each cycle. | |||
off_color (:class:`Color`): The color to flash to for the off state. | |||
Returns: | |||
:class:`Color` instance. | |||
''' | |||
flasher = copy.copy(self) | |||
flasher.on_period_ms = on_period_ms | |||
flasher.off_period_ms = off_period_ms | |||
flasher.off_color=off_color | |||
return flasher | |||
def _set_light(msg, idx, light): | |||
# For use with clad light messages specifically. | |||
if not isinstance(light, Light): | |||
raise TypeError("Expected a lights.Light") | |||
msg.onColor[idx] = light.on_color.int_color | |||
msg.offColor[idx] = light.off_color.int_color | |||
msg.onPeriod_ms[idx] = light.on_period_ms | |||
msg.offPeriod_ms[idx] = light.off_period_ms | |||
msg.transitionOnPeriod_ms[idx] = light.transition_on_period_ms | |||
msg.transitionOffPeriod_ms[idx] = light.transition_off_period_ms | |||
#There is a glitch so it will always flash unless on_color==off_color | |||
#ticket is COZMO-3319 | |||
#: :class:`Light`: A steady green colored LED light. | |||
green_light = Light(on_color=green, off_color=green) | |||
#: :class:`Light`: A steady red colored LED light. | |||
red_light = Light(on_color=red, off_color=red) | |||
#: :class:`Light`: A steady blue colored LED light. | |||
blue_light = Light(on_color=blue, off_color=blue) | |||
#: :class:`Light`: A steady white colored LED light. | |||
white_light = Light(on_color=white, off_color=white) | |||
#: :class:`Light`: A steady off (non-illuminated LED light). | |||
off_light = Light(on_color=off, off_color=off) |
@@ -0,0 +1,322 @@ | |||
# Copyright (c) 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. | |||
'''A 2D navigation memory map of the world around Cozmo. | |||
Cozmo builds a memory map of the navigable world around him as he drives | |||
around. This is mostly based on where objects are seen (the cubes, charger, and | |||
any custom objects), and also includes where Cozmo detects cliffs/drops, and | |||
visible edges (e.g. sudden changes in color). | |||
This differs from a standard occupancy map in that it doesn't deal with | |||
probabilities of occupancy, but instead encodes what type of content is there. | |||
To use the map you must first call :meth:`cozmo.world.World.request_nav_memory_map` | |||
with a positive frequency so that the data is streamed to the SDK. | |||
''' | |||
# __all__ should order by constants, event classes, other classes, functions. | |||
__all__ = ['EvtNewNavMemoryMap', | |||
'NavMemoryMapGrid', 'NavMemoryMapGridNode', | |||
'NodeContentTypes'] | |||
from collections import namedtuple | |||
from . import event | |||
from . import logger | |||
from . import util | |||
from ._clad import CladEnumWrapper, _clad_to_game_iface | |||
class EvtNewNavMemoryMap(event.Event): | |||
'''Dispatched when a new memory map is received.''' | |||
nav_memory_map = 'A NavMemoryMapGrid object' | |||
class _NodeContentType(namedtuple('_NodeContentType', 'name id')): | |||
# Tuple mapping between CLAD ENodeContentTypeEnum name and ID | |||
# All instances will be members of ActionResults | |||
# Keep _NodeContentType as lightweight as a normal namedtuple | |||
__slots__ = () | |||
def __str__(self): | |||
return 'NodeContentTypes.%s' % self.name | |||
class NodeContentTypes(CladEnumWrapper): | |||
"""The content types for a :class:`NavMemoryMapGridNode`.""" | |||
_clad_enum = _clad_to_game_iface.ENodeContentTypeEnum | |||
_entry_type = _NodeContentType | |||
#: The contents of the node is unknown. | |||
Unknown = _entry_type("Unknown", _clad_enum.Unknown) | |||
#: The node is clear of obstacles, because Cozmo has seen objects on the | |||
#: other side, but it might contain a cliff. The node will be marked as | |||
#: either :attr:`Cliff` or :attr:`ClearOfCliff` once Cozmo has driven there. | |||
ClearOfObstacle = _entry_type("ClearOfObstacle", _clad_enum.ClearOfObstacle) | |||
#: The node is clear of any cliffs (a sharp drop) or obstacles. | |||
ClearOfCliff = _entry_type("ClearOfCliff", _clad_enum.ClearOfCliff) | |||
#: The node contains a :class:`~cozmo.objects.LightCube`. | |||
ObstacleCube = _entry_type("ObstacleCube", _clad_enum.ObstacleCube) | |||
#: The node contains a :class:`~cozmo.objects.Charger`. | |||
ObstacleCharger = _entry_type("ObstacleCharger", _clad_enum.ObstacleCharger) | |||
#: The node contains a cliff (a sharp drop). | |||
Cliff = _entry_type("Cliff", _clad_enum.Cliff) | |||
#: The node contains a visible edge (based on the camera feed). | |||
VisionBorder = _entry_type("VisionBorder", _clad_enum.VisionBorder) | |||
# This entry is undocumented and not currently used | |||
_ObstacleProx = _entry_type("ObstacleProx", _clad_enum.ObstacleProx) | |||
NodeContentTypes._init_class() | |||
class NavMemoryMapGridNode: | |||
"""A node in a :class:`NavMemoryMapGrid`. | |||
Leaf nodes contain content, all other nodes are split into 4 equally sized | |||
children. | |||
Child node indices are stored in the following X,Y orientation: | |||
+---+----+---+ | |||
| ^ | 2 | 0 | | |||
+---+----+---+ | |||
| Y | 3 | 1 | | |||
+---+----+---+ | |||
| | X->| | | |||
+---+----+---+ | |||
""" | |||
def __init__(self, depth, size, center, parent): | |||
#: int: The depth of this node. I.e. how far down the quad-tree is it. | |||
self.depth = depth | |||
#: float: The size (width or length) of this square node. | |||
self.size = size | |||
#: :class:`~cozmo.util.Vector3`: The center of this node. | |||
self.center = center # type: util.Vector3 | |||
#: :class:`NavMemoryMapGridNode`: The parent of this node. Is ``None`` for the root node. | |||
self.parent = parent # type: NavMemoryMapGridNode | |||
#: list of :class:`NavMemoryMapGridNode`: ``None`` for leaf nodes, a list of 4 | |||
#: child nodes otherwise. | |||
self.children = None | |||
#: An attribute of :class:`NodeContentTypes`: The content type in this | |||
#: node. Only leaf nodes have content, this is ``None`` for all other | |||
#: nodes. | |||
self.content = None # type: _NodeContentType | |||
self._next_child = 0 # Used when building to track which branch to follow | |||
def __repr__(self): | |||
return '<%s center: %s size: %s content: %s>' % ( | |||
self.__class__.__name__, self.center, self.size, self.content) | |||
def contains_point(self, x, y): | |||
"""Test if the node contains the given x,y coordinates. | |||
Args: | |||
x (float): x coordinate for the point | |||
y (float): y coordinate for the point | |||
Returns: | |||
bool: True if the node contains the point, False otherwise. | |||
""" | |||
half_size = self.size * 0.5 | |||
dist_x = abs(self.center.x - x) | |||
dist_y = abs(self.center.y - y) | |||
return (dist_x <= half_size) and (dist_y <= half_size) | |||
def _get_node(self, x, y, assumed_in_bounds): | |||
if not assumed_in_bounds and not self.contains_point(x, y): | |||
# point is out of bounds | |||
return None | |||
if self.children is None: | |||
return self | |||
else: | |||
x_offset = 2 if x < self.center.x else 0 | |||
y_offset = 1 if y < self.center.y else 0 | |||
child_node = self.children[x_offset+y_offset] | |||
# child node is by definition in bounds / on boundary | |||
return child_node._get_node(x, y, True) | |||
def get_node(self, x, y): | |||
"""Get the node at the given x,y coordinates. | |||
Args: | |||
x (float): x coordinate for the point | |||
y (float): y coordinate for the point | |||
Returns: | |||
:class:`NavMemoryMapGridNode`: The smallest node that includes the | |||
point. Will be ``None`` if the point is outside of the map. | |||
""" | |||
return self._get_node(x, y, assumed_in_bounds=False) | |||
def get_content(self, x, y): | |||
"""Get the node's content at the given x,y coordinates. | |||
Args: | |||
x (float): x coordinate for the point | |||
y (float): y coordinate for the point | |||
Returns: | |||
:class:`_NodeContentType`: The content included at that point. | |||
Will be :attr:`NodeContentTypes.Unknown` if the point is outside of | |||
the map. | |||
""" | |||
node = self.get_node(x, y) | |||
if node: | |||
return node.content | |||
else: | |||
return NodeContentTypes.Unknown | |||
def _add_child(self, content, depth): | |||
"""Add a child node to the quad tree. | |||
The quad-tree is serialized to a flat list of nodes, we deserialize | |||
back to a quad-tree structure here, with the depth of each node | |||
indicating where it is placed. | |||
Args: | |||
content (:class:`_NodeContentType`): The content to store in the leaf node | |||
depth (int): The depth that this leaf node is located at. | |||
Returns: | |||
bool: True if parent should use the next child for future _add_child | |||
calls (this is an internal implementation detail of h | |||
""" | |||
if depth > self.depth: | |||
logger.error("NavMemoryMapGridNode depth %s > %s", depth, self.depth) | |||
if self._next_child > 3: | |||
logger.error("NavMemoryMapGridNode _next_child %s (>3) at depth %s", self._next_child, self.depth) | |||
if self.depth == depth: | |||
if self.content is not None: | |||
logger.error("NavMemoryMapGridNode: Clobbering %s at depth %s with %s", | |||
self.content, self.depth, content) | |||
self.content = content | |||
# This node won't be further subdivided, and is now full | |||
return True | |||
if self.children is None: | |||
# Create 4 child nodes for quad-tree structure | |||
next_depth = self.depth - 1 | |||
next_size = self.size * 0.5 | |||
offset = next_size * 0.5 | |||
center1 = util.Vector3(self.center.x + offset, self.center.y + offset, self.center.z) | |||
center2 = util.Vector3(self.center.x + offset, self.center.y - offset, self.center.z) | |||
center3 = util.Vector3(self.center.x - offset, self.center.y + offset, self.center.z) | |||
center4 = util.Vector3(self.center.x - offset, self.center.y - offset, self.center.z) | |||
self.children = [NavMemoryMapGridNode(next_depth, next_size, center1, self), | |||
NavMemoryMapGridNode(next_depth, next_size, center2, self), | |||
NavMemoryMapGridNode(next_depth, next_size, center3, self), | |||
NavMemoryMapGridNode(next_depth, next_size, center4, self)] | |||
if self.children[self._next_child]._add_child(content, depth): | |||
# Child node is now full, start using the next child | |||
self._next_child += 1 | |||
if self._next_child > 3: | |||
# All children are now full - parent should start using the next child | |||
return True | |||
else: | |||
# Empty children remain - parent can keep using this child | |||
return False | |||
class NavMemoryMapGrid: | |||
"""A navigation memory map, stored as a quad-tree.""" | |||
def __init__(self, origin_id, root_depth, root_size, root_center_x, root_center_y): | |||
#: int: The origin ID for the map. Only maps and :class:`~cozmo.util.Pose` | |||
#: objects of the same origin ID are in the same coordinate frame and | |||
#: can therefore be compared. | |||
self.origin_id = origin_id | |||
root_center = util.Vector3(root_center_x, root_center_y, 0.0) | |||
self._root_node = NavMemoryMapGridNode(root_depth, root_size, root_center, None) | |||
def __repr__(self): | |||
return '<%s center: %s size: %s>' % ( | |||
self.__class__.__name__, self.center, self.size) | |||
@property | |||
def root_node(self): | |||
""":class:`NavMemoryMapGridNode`: The root node for the grid, contains all other nodes.""" | |||
return self._root_node | |||
@property | |||
def size(self): | |||
"""float: The size (width or length) of the square grid.""" | |||
return self._root_node.size | |||
@property | |||
def center(self): | |||
""":class:`~cozmo.util.Vector3`: The center of this map.""" | |||
return self._root_node.center | |||
def contains_point(self, x, y): | |||
"""Test if the map contains the given x,y coordinates. | |||
Args: | |||
x (float): x coordinate for the point | |||
y (float): y coordinate for the point | |||
Returns: | |||
bool: True if the map contains the point, False otherwise. | |||
""" | |||
return self._root_node.contains_point(x,y) | |||
def get_node(self, x, y): | |||
"""Get the node at the given x,y coordinates. | |||
Args: | |||
x (float): x coordinate for the point | |||
y (float): y coordinate for the point | |||
Returns: | |||
:class:`NavMemoryMapGridNode`: The smallest node that includes the | |||
point. Will be ``None`` if the point is outside of the map. | |||
""" | |||
return self._root_node.get_node(x, y) | |||
def get_content(self, x, y): | |||
"""Get the map's content at the given x,y coordinates. | |||
Args: | |||
x (float): x coordinate for the point | |||
y (float): y coordinate for the point | |||
Returns: | |||
:class:`_NodeContentType`: The content included at that point. | |||
Will be :attr:`NodeContentTypes.Unknown` if the point is outside of | |||
the map. | |||
""" | |||
return self._root_node.get_content(x, y) | |||
def _add_quad(self, content, depth): | |||
# Convert content int to our enum representation | |||
content = NodeContentTypes.find_by_id(content) | |||
self._root_node._add_child(content, depth) |
@@ -0,0 +1,939 @@ | |||
# 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. | |||
'''Object and Power Cube recognition. | |||
Cozmo can recognize and track a number of different types of objects. | |||
These objects may be visible (currently observed by the robot's camera) | |||
and tappable (in the case of the Power Cubes that ship with the robot). | |||
Power Cubes are known as a :class:`LightCube` by the SDK. Each cube has | |||
controllable lights, and sensors that can determine when its being moved | |||
or tapped. | |||
Objects can emit several events such as :class:`EvtObjectObserved` when | |||
the robot sees (or continues to see) the object with its camera, or | |||
:class:`EvtObjectTapped` if a power cube is tapped by a player. You | |||
can either observe the object's instance directly, or capture all such events | |||
for all objects by observing them on :class:`cozmo.world.World` instead. | |||
All observable objects have a marker attached to them, which allows Cozmo | |||
to recognize the object and it's position and rotation("pose"). You can attach | |||
markers to your own objects for Cozmo to recognize by printing them out from the | |||
online documentation. They will be detected as :class:`CustomObject` instances. | |||
''' | |||
# __all__ should order by constants, event classes, other classes, functions. | |||
__all__ = ['LightCube1Id', 'LightCube2Id', 'LightCube3Id', 'LightCubeIDs', | |||
'OBJECT_VISIBILITY_TIMEOUT', | |||
'EvtObjectAppeared', | |||
'EvtObjectConnectChanged', 'EvtObjectConnected', | |||
'EvtObjectDisappeared', 'EvtObjectLocated', | |||
'EvtObjectMoving', 'EvtObjectMovingStarted', 'EvtObjectMovingStopped', | |||
'EvtObjectObserved', 'EvtObjectTapped', | |||
'ObservableElement', 'ObservableObject', 'LightCube', 'Charger', | |||
'CustomObject', 'CustomObjectMarkers', 'CustomObjectTypes', 'FixedCustomObject'] | |||
import collections | |||
import math | |||
import time | |||
from . import logger | |||
from . import action | |||
from . import event | |||
from . import lights | |||
from . import util | |||
from ._clad import _clad_to_engine_iface, _clad_to_game_cozmo, _clad_to_engine_cozmo, _clad_to_game_anki | |||
#: Length of time in seconds to go without receiving an observed event before | |||
#: assuming that Cozmo can no longer see an object. | |||
OBJECT_VISIBILITY_TIMEOUT = 0.4 | |||
class EvtObjectObserved(event.Event): | |||
'''Triggered whenever an object is visually identified by the robot. | |||
A stream of these events are produced while an object is visible to the robot. | |||
Each event has an updated image_box field. | |||
See EvtObjectAppeared if you only want to know when an object first | |||
becomes visible. | |||
''' | |||
obj = 'The object that was observed' | |||
updated = 'A set of field names that have changed' | |||
image_box = 'A comzo.util.ImageBox defining where the object is within Cozmo\'s camera view' | |||
pose = 'The cozmo.util.Pose defining the position and rotation of the object' | |||
class EvtObjectAppeared(event.Event): | |||
'''Triggered whenever an object is first visually identified by a robot. | |||
This differs from EvtObjectObserved in that it's only triggered when | |||
an object initially becomes visible. If it disappears for more than | |||
OBJECT_VISIBILITY_TIMEOUT seconds and then is seen again, a | |||
EvtObjectDisappeared will be dispatched, followed by another | |||
EvtObjectAppeared event. | |||
For continuous tracking information about a visible object, see | |||
EvtObjectObserved. | |||
''' | |||
obj = 'The object that was observed' | |||
updated = 'A set of field names that have changed' | |||
image_box = 'A comzo.util.ImageBox defining where the object is within Cozmo\'s camera view' | |||
pose = 'The cozmo.util.Pose defining the position and rotation of the object' | |||
class EvtObjectConnected(event.Event): | |||
'''Triggered when the engine reports that an object is connected (i.e. exists). | |||
This will usually occur at the start of the program in response to the SDK | |||
sending RequestConnectedObjects to the engine. | |||
''' | |||
obj = 'The object that is connected' | |||
connected = 'True if the object connected, False if it disconnected' | |||
class EvtObjectConnectChanged(event.Event): | |||
'Triggered when an active object has connected or disconnected from the robot.' | |||
obj = 'The object that connected or disconnected' | |||
connected = 'True if the object connected, False if it disconnected' | |||
class EvtObjectLocated(event.Event): | |||
'''Triggered when the engine reports that an object is located (i.e. pose is known). | |||
This will usually occur at the start of the program in response to the SDK | |||
sending RequestLocatedObjectStates to the engine. | |||
''' | |||
obj = 'The object that is located' | |||
updated = 'A set of field names that have changed' | |||
pose = 'The cozmo.util.Pose defining the position and rotation of the object' | |||
class EvtObjectDisappeared(event.Event): | |||
'''Triggered whenever an object that was previously being observed is no longer visible.''' | |||
obj = 'The object that is no longer being observed' | |||
class EvtObjectMoving(event.Event): | |||
'Triggered when an active object is currently moving.' | |||
obj = 'The object that is currently moving' | |||
# :class:`~cozmo.util.Vector3`: The currently measured acceleration | |||
acceleration = 'The currently measured acceleration' | |||
move_duration = 'The current duration of time (in seconds) that the object has spent moving' | |||
class EvtObjectMovingStarted(event.Event): | |||
'Triggered when an active object starts moving.' | |||
obj = 'The object that started moving' | |||
#: :class:`~cozmo.util.Vector3`: The currently measured acceleration | |||
acceleration = 'The currently measured acceleration' | |||
class EvtObjectMovingStopped(event.Event): | |||
'Triggered when an active object stops moving.' | |||
obj = 'The object that stopped moving' | |||
move_duration = 'The duration of time (in seconds) that the object spent moving' | |||
class EvtObjectTapped(event.Event): | |||
'Triggered when an active object is tapped.' | |||
obj = 'The object that was tapped' | |||
tap_count = 'Number of taps detected' | |||
tap_duration = 'The duration of the tap in ms' | |||
tap_intensity = 'The intensity of the tap' | |||
class ObservableElement(event.Dispatcher): | |||
'''The base type for anything Cozmo can see.''' | |||
#: Length of time in seconds to go without receiving an observed event before | |||
#: assuming that Cozmo can no longer see an element. Can be overridden in sub | |||
#: classes. | |||
visibility_timeout = OBJECT_VISIBILITY_TIMEOUT | |||
def __init__(self, conn, world, robot, **kw): | |||
super().__init__(**kw) | |||
self._robot = robot | |||
self._pose = None | |||
self.conn = conn | |||
#: :class:`cozmo.world.World`: The robot's world in which this element is located. | |||
self.world = world | |||
#: float: The time the last event was received. | |||
#: ``None`` if no events have yet been received. | |||
self.last_event_time = None | |||
#: float: The time the element was last observed by the robot. | |||
#: ``None`` if the element has not yet been observed. | |||
self.last_observed_time = None | |||
#: int: The robot's timestamp of the last observed event. | |||
#: ``None`` if the element has not yet been observed. | |||
#: In milliseconds relative to robot epoch. | |||
self.last_observed_robot_timestamp = None | |||
#: :class:`~cozmo.util.ImageBox`: The ImageBox defining where the | |||
#: object was last visible within Cozmo's camera view. | |||
#: ``None`` if the element has not yet been observed. | |||
self.last_observed_image_box = None | |||
self._is_visible = False | |||
self._observed_timeout_handler = None | |||
def __repr__(self): | |||
extra = self._repr_values() | |||
if len(extra) > 0: | |||
extra = ' '+extra | |||
if self.pose: | |||
extra += ' pose=%s' % self.pose | |||
return '<%s%s is_visible=%s>' % (self.__class__.__name__, | |||
extra, self.is_visible) | |||
#### Private Methods #### | |||
def _repr_values(self): | |||
return '' | |||
def _update_field(self, changed, field_name, new_value): | |||
# Set only changed fields and update the passed in changed set | |||
current = getattr(self, field_name) | |||
if current != new_value: | |||
setattr(self, field_name, new_value) | |||
changed.add(field_name) | |||
def _reset_observed_timeout_handler(self): | |||
if self._observed_timeout_handler is not None: | |||
self._observed_timeout_handler.cancel() | |||
self._observed_timeout_handler = self._loop.call_later( | |||
self.visibility_timeout, self._observed_timeout) | |||
def _observed_timeout(self): | |||
# triggered when the element is no longer considered "visible" | |||
# ie. visibility_timeout seconds after the last observed event | |||
self._is_visible = False | |||
self._dispatch_disappeared_event() | |||
def _dispatch_observed_event(self, changed_fields, image_box): | |||
# Override in subclass if there is a specific event for that type | |||
pass | |||
def _dispatch_appeared_event(self, changed_fields, image_box): | |||
# Override in subclass if there is a specific event for that type | |||
pass | |||
def _dispatch_disappeared_event(self): | |||
# Override in subclass if there is a specific event for that type | |||
pass | |||
def _on_observed(self, image_box, timestamp, changed_fields): | |||
# Called from subclasses on their corresponding observed messages | |||
newly_visible = self._is_visible is False | |||
self._is_visible = True | |||
changed_fields |= {'last_observed_time', 'last_observed_robot_timestamp', | |||
'last_event_time', 'last_observed_image_box'} | |||
now = time.time() | |||
self.last_observed_time = now | |||
self.last_observed_robot_timestamp = timestamp | |||
self.last_event_time = now | |||
self.last_observed_image_box = image_box | |||
self._reset_observed_timeout_handler() | |||
self._dispatch_observed_event(changed_fields, image_box) | |||
if newly_visible: | |||
self._dispatch_appeared_event(changed_fields, image_box) | |||
#### Properties #### | |||
@property | |||
def pose(self): | |||
''':class:`cozmo.util.Pose`: The pose of the element in the world. | |||
Is ``None`` for elements that don't have pose information. | |||
''' | |||
return self._pose | |||
@property | |||
def time_since_last_seen(self): | |||
'''float: time since this element was last seen (math.inf if never)''' | |||
if self.last_observed_time is None: | |||
return math.inf | |||
return time.time() - self.last_observed_time | |||
@property | |||
def is_visible(self): | |||
'''bool: True if the element has been observed recently. | |||
"recently" is defined as :attr:`visibility_timeout` seconds. | |||
''' | |||
return self._is_visible | |||
class ObservableObject(ObservableElement): | |||
'''The base type for objects in Cozmo's world. | |||
See parent class :class:`ObservableElement` for additional properties | |||
and methods. | |||
''' | |||
#: bool: True if this type of object can be physically picked up by Cozmo | |||
pickupable = False | |||
#: bool: True if this type of object can have objects physically placed on it by Cozmo | |||
place_objects_on_this = False | |||
def __init__(self, conn, world, object_id=None, **kw): | |||
super().__init__(conn, world, robot=None, **kw) | |||
self._object_id = object_id | |||
#### Private Methods #### | |||
def _repr_values(self): | |||
return 'object_id=%s' % self.object_id | |||
def _dispatch_observed_event(self, changed_fields, image_box): | |||
self.dispatch_event(EvtObjectObserved, obj=self, | |||
updated=changed_fields, image_box=image_box, pose=self._pose) | |||
def _dispatch_appeared_event(self, changed_fields, image_box): | |||
self.dispatch_event(EvtObjectAppeared, obj=self, | |||
updated=changed_fields, image_box=image_box, pose=self._pose) | |||
def _dispatch_disappeared_event(self): | |||
self.dispatch_event(EvtObjectDisappeared, obj=self) | |||
def _handle_connected_object_state(self, object_state): | |||
# triggered when engine sends a ConnectedObjectStates message | |||
# as a response to a RequestConnectedObjects message | |||
self._pose = util.Pose._create_default() | |||
self.is_connected = True | |||
self.dispatch_event(EvtObjectConnected, obj=self) | |||
def _handle_located_object_state(self, object_state): | |||
# triggered when engine sends a LocatedObjectStates message | |||
# as a response to a RequestLocatedObjectStates message | |||
if (self.last_observed_robot_timestamp and | |||
(self.last_observed_robot_timestamp > object_state.lastObservedTimestamp)): | |||
logger.warning("Ignoring old located object_state=%s obj=%s (last_observed_robot_timestamp=%s)", | |||
object_state, self, self.last_observed_robot_timestamp) | |||
return | |||
changed_fields = {'last_observed_robot_timestamp', 'pose'} | |||
self.last_observed_robot_timestamp = object_state.lastObservedTimestamp | |||
self._pose = util.Pose._create_from_clad(object_state.pose) | |||
if object_state.poseState == _clad_to_game_anki.PoseState.Invalid: | |||
logger.error("Unexpected Invalid pose state received") | |||
self._pose.invalidate() | |||
elif object_state.poseState == _clad_to_game_anki.PoseState.Dirty: | |||
# Note Dirty currently means either moved (in which case it's really dirty) | |||
# or inaccurate (e.g. seen from too far away to give an accurate enough pose for localization) | |||
# TODO: split Dirty into 2 states, and allow SDK to report the distinction. | |||
self._pose._is_accurate = False | |||
self.dispatch_event(EvtObjectLocated, | |||
obj=self, | |||
updated=changed_fields, | |||
pose=self._pose) | |||
#### Properties #### | |||
@property | |||
def object_id(self): | |||
'''int: The internal ID assigned to the object. | |||
This value can only be assigned once as it is static in the engine. | |||
''' | |||
return self._object_id | |||
@object_id.setter | |||
def object_id(self, value): | |||
if self._object_id is not None: | |||
# We cannot currently rely on Engine ensuring that object ID remains static | |||
# E.g. in the case of a cube disconnecting and reconnecting it's removed | |||
# and then re-added to blockworld which results in a new ID. | |||
logger.warning("Changing object_id for %s from %s to %s", self.__class__, self._object_id, value) | |||
else: | |||
logger.debug("Setting object_id for %s to %s", self.__class__, value) | |||
self._object_id = value | |||
@property | |||
def descriptive_name(self): | |||
'''str: A descriptive name for this ObservableObject instance.''' | |||
# Note: Sub-classes should override this to add any other relevant info | |||
# for that object type. | |||
return "%s id=%d" % (self.__class__.__name__, self.object_id) | |||
#### Private Event Handlers #### | |||
def _recv_msg_robot_observed_object(self, evt, *, msg): | |||
changed_fields = {'pose'} | |||
self._pose = util.Pose._create_from_clad(msg.pose) | |||
image_box = util.ImageBox._create_from_clad_rect(msg.img_rect) | |||
self._on_observed(image_box, msg.timestamp, changed_fields) | |||
#### Public Event Handlers #### | |||
#### Event Wrappers #### | |||
#### Commands #### | |||
#: LightCube1Id's markers look a bit like a paperclip | |||
LightCube1Id = _clad_to_game_cozmo.ObjectType.Block_LIGHTCUBE1 | |||
#: LightCube2Id's markers look a bit like a lamp (or a heart) | |||
LightCube2Id = _clad_to_game_cozmo.ObjectType.Block_LIGHTCUBE2 | |||
#: LightCube3Id's markers look a bit like the letters 'ab' over 'T' | |||
LightCube3Id = _clad_to_game_cozmo.ObjectType.Block_LIGHTCUBE3 | |||
#: An ordered list of the 3 light cube IDs for convenience | |||
LightCubeIDs = [LightCube1Id, LightCube2Id, LightCube3Id] | |||
class LightCube(ObservableObject): | |||
'''A light cube object has four LEDs that Cozmo can actively manipulate and communicate with. | |||
See parent class :class:`ObservableObject` for additional properties | |||
and methods. | |||
''' | |||
#TODO investigate why the top marker orientation of a cube is a bit strange | |||
#: Voltage where a cube's battery can be considered empty | |||
EMPTY_VOLTAGE = 1.0 | |||
#: Voltage where a cube's battery can be considered full | |||
FULL_VOLTAGE = 1.5 | |||
pickupable = True | |||
place_objects_on_this = True | |||
def __init__(self, cube_id, *a, **kw): | |||
super().__init__(*a, **kw) | |||
#: float: The time the object was last tapped | |||
#: ``None`` if the cube wasn't tapped yet. | |||
self.last_tapped_time = None | |||
#: int: The robot's timestamp of the last tapped event. | |||
#: ``None`` if the cube wasn't tapped yet. | |||
#: In milliseconds relative to robot epoch. | |||
self.last_tapped_robot_timestamp = None | |||
#: float: The time the object was last moved | |||
#: ``None`` if the cube wasn't moved yet. | |||
self.last_moved_time = None | |||
#: float: The time the object started moving when last moved | |||
self.last_moved_start_time = None | |||
#: int: The robot's timestamp of the last move event. | |||
#: ``None`` if the cube wasn't moved yet. | |||
#: In milliseconds relative to robot epoch. | |||
self.last_moved_robot_timestamp = None | |||
#: int: The robot's timestamp of when the object started moving when last moved | |||
#: ``None`` if the cube wasn't moved yet. | |||
#: In milliseconds relative to robot epoch. | |||
self.last_moved_start_robot_timestamp = None | |||
#: float: Battery voltage. | |||
#: ``None`` if no voltage reading has been received yet | |||
self.battery_voltage = None | |||
#: bool: True if the cube's accelerometer indicates that the cube is moving. | |||
self.is_moving = False | |||
#: bool: True if the cube is currently connected to the robot via radio. | |||
self.is_connected = False | |||
self._cube_id = cube_id | |||
def _repr_values(self): | |||
super_values = super()._repr_values() | |||
if len(super_values) > 0: | |||
super_values += ' ' | |||
return ('{super_values}' | |||
'battery={self.battery_str:s}'.format(self=self, super_values=super_values)) | |||
#### Private Methods #### | |||
def _set_light(self, msg, idx, light): | |||
if not isinstance(light, lights.Light): | |||
raise TypeError("Expected a lights.Light") | |||
msg.onColor[idx] = light.on_color.int_color | |||
msg.offColor[idx] = light.off_color.int_color | |||
msg.onPeriod_ms[idx] = light.on_period_ms | |||
msg.offPeriod_ms[idx] = light.off_period_ms | |||
msg.transitionOnPeriod_ms[idx] = light.transition_on_period_ms | |||
msg.transitionOffPeriod_ms[idx] = light.transition_off_period_ms | |||
#### Event Wrappers #### | |||
async def wait_for_tap(self, timeout=None): | |||
'''Wait for the object to receive a tap event. | |||
Args: | |||
timeout (float): Maximum time to wait for a tap, in seconds. None for indefinite | |||
Returns: | |||
A :class:`EvtObjectTapped` object if a tap was received. | |||
''' | |||
return await self.wait_for(EvtObjectTapped, timeout=timeout) | |||
#### Properties #### | |||
@property | |||
def battery_percentage(self): | |||
"""float: Battery level as a percentage.""" | |||
if self.battery_voltage is None: | |||
# not received a voltage measurement yet | |||
return None | |||
elif self.battery_voltage >= self.FULL_VOLTAGE: | |||
return 100.0 | |||
elif self.battery_voltage <= self.EMPTY_VOLTAGE: | |||
return 0.0 | |||
else: | |||
return 100.0 * ((self.battery_voltage - self.EMPTY_VOLTAGE) / | |||
(self.FULL_VOLTAGE - self.EMPTY_VOLTAGE)) | |||
@property | |||
def battery_str(self): | |||
"""str: String representation of the battery level.""" | |||
if self.battery_voltage is None: | |||
return "Unknown" | |||
else: | |||
return ('{self.battery_percentage:.0f}%'.format(self=self)) | |||
@property | |||
def cube_id(self): | |||
"""int: The Light Cube ID. | |||
This will be one of :attr:`~cozmo.objects.LightCube1Id`, | |||
:attr:`~cozmo.objects.LightCube2Id` and :attr:`~cozmo.objects.LightCube3Id`. | |||
Note: the cube_id is not the same thing as the object_id. | |||
""" | |||
return self._cube_id | |||
#### Private Event Handlers #### | |||
def _recv_msg_object_tapped(self, evt, *, msg): | |||
now = time.time() | |||
self.last_event_time = now | |||
self.last_tapped_time = now | |||
self.last_tapped_robot_timestamp = msg.timestamp | |||
tap_intensity = msg.tapPos - msg.tapNeg | |||
self.dispatch_event(EvtObjectTapped, obj=self, | |||
tap_count=msg.numTaps, tap_duration=msg.tapTime, tap_intensity=tap_intensity) | |||
def _recv_msg_object_moved(self, evt, *, msg): | |||
now = time.time() | |||
started_moving = not self.is_moving | |||
self.is_moving = True | |||
self.last_event_time = now | |||
self.last_moved_time = now | |||
self.last_moved_robot_timestamp = msg.timestamp | |||
self.pose.invalidate() | |||
acceleration = util.Vector3(msg.accel.x, msg.accel.y, msg.accel.z) | |||
if started_moving: | |||
self.last_moved_start_time = now | |||
self.last_moved_start_robot_timestamp = msg.timestamp | |||
self.dispatch_event(EvtObjectMovingStarted, obj=self, | |||
acceleration=acceleration) | |||
else: | |||
move_duration = now - self.last_moved_start_time | |||
self.dispatch_event(EvtObjectMoving, obj=self, | |||
acceleration=acceleration, | |||
move_duration=move_duration) | |||
def _recv_msg_object_stopped_moving(self, evt, *, msg): | |||
now = time.time() | |||
if self.is_moving: | |||
self.is_moving = False | |||
move_duration = now - self.last_moved_start_time | |||
else: | |||
# This happens for very short movements that are immediately | |||
# considered stopped (no acceleration info is present) | |||
move_duration = 0.0 | |||
self.dispatch_event(EvtObjectMovingStopped, obj=self, | |||
move_duration=move_duration) | |||
def _recv_msg_object_power_level(self, evt, *, msg): | |||
self.battery_voltage = msg.batteryLevel * 0.01 | |||
def _recv_msg_object_connection_state(self, evt, *, msg): | |||
if self.is_connected != msg.connected: | |||
if msg.connected: | |||
logger.info("Object connected: %s", self) | |||
else: | |||
logger.info("Object disconnected: %s", self) | |||
self.is_connected = msg.connected | |||
self.dispatch_event(EvtObjectConnectChanged, obj=self, | |||
connected=self.is_connected) | |||
@property | |||
def descriptive_name(self): | |||
'''str: A descriptive name for this LightCube instance.''' | |||
# Specialization of ObservableObject's method to include the cube ID. | |||
return "%s %s id=%d" % (self.__class__.__name__, self._cube_id, self.object_id) | |||
#### Public Event Handlers #### | |||
def recv_evt_object_tapped(self, evt, **kw): | |||
pass | |||
#### Commands #### | |||
# TODO: make this explicit as to which light goes to which corner. | |||
def set_light_corners(self, light1, light2, light3, light4): | |||
"""Set the light for each corner""" | |||
msg = _clad_to_engine_iface.SetAllActiveObjectLEDs(objectID=self.object_id) | |||
for i, light in enumerate( (light1, light2, light3, light4) ): | |||
if light is not None: | |||
lights._set_light(msg, i, light) | |||
self.conn.send_msg(msg) | |||
def set_lights(self, light): | |||
'''Set all lights on the cube | |||
Args: | |||
light (:class:`cozmo.lights.Light`): The settings for the lights. | |||
''' | |||
msg = _clad_to_engine_iface.SetAllActiveObjectLEDs( | |||
objectID=self.object_id) | |||
for i in range(4): | |||
lights._set_light(msg, i, light) | |||
self.conn.send_msg(msg) | |||
def set_lights_off(self): | |||
'''Turn off all the lights on the cube.''' | |||
self.set_lights(lights.off_light) | |||
class Charger(ObservableObject): | |||
'''Cozmo's charger object, which the robot can observe and drive toward. | |||
See parent class :class:`ObservableObject` for additional properties | |||
and methods. | |||
''' | |||
def __init__(self, *a, **kw): | |||
super().__init__(*a, **kw) | |||
class CustomObject(ObservableObject): | |||
'''An object defined by the SDK. It is bound to a specific objectType e.g ``CustomType00``. | |||
This defined object is given a size in the x,y and z axis. The dimensions | |||
of the markers on the object are also defined. We get an | |||
:class:`cozmo.objects.EvtObjectObserved` message when the robot sees these | |||
markers. | |||
See parent class :class:`ObservableObject` for additional properties | |||
and methods. | |||
These objects are created automatically by the engine when Cozmo observes | |||
an object with custom markers. For Cozmo to see one of these you must first | |||
define an object with custom markers, via one of the following methods: | |||
:meth:`~cozmo.world.World.define_custom_box`. | |||
:meth:`~cozmo.world.World.define_custom_cube`, or | |||
:meth:`~cozmo.world.World.define_custom_wall` | |||
''' | |||
def __init__(self, conn, world, object_type, | |||
x_size_mm, y_size_mm, z_size_mm, | |||
marker_width_mm, marker_height_mm, is_unique, **kw): | |||
super().__init__(conn, world, **kw) | |||
self.object_type = object_type | |||
self._x_size_mm = x_size_mm | |||
self._y_size_mm = y_size_mm | |||
self._z_size_mm = z_size_mm | |||
self._marker_width_mm = marker_width_mm | |||
self._marker_height_mm = marker_height_mm | |||
self._is_unique = is_unique | |||
def _repr_values(self): | |||
return ('object_type={self.object_type} ' | |||
'x_size_mm={self.x_size_mm:.1f} ' | |||
'y_size_mm={self.y_size_mm:.1f} ' | |||
'z_size_mm={self.z_size_mm:.1f} ' | |||
'is_unique={self.is_unique}'.format(self=self)) | |||
#### Private Methods #### | |||
#### Event Wrappers #### | |||
#### Properties #### | |||
@property | |||
def x_size_mm(self): | |||
'''float: Size of this object in its X axis, in millimeters.''' | |||
return self._x_size_mm | |||
@property | |||
def y_size_mm(self): | |||
'''float: Size of this object in its Y axis, in millimeters.''' | |||
return self._y_size_mm | |||
@property | |||
def z_size_mm(self): | |||
'''float: Size of this object in its Z axis, in millimeters.''' | |||
return self._z_size_mm | |||
@property | |||
def marker_width_mm(self): | |||
'''float: Width in millimeters of the marker on this object.''' | |||
return self._marker_width_mm | |||
@property | |||
def marker_height_mm(self): | |||
'''float: Height in millimeters of the marker on this object.''' | |||
return self._marker_height_mm | |||
@property | |||
def is_unique(self): | |||
'''bool: True if there should only be one of this object type in the world.''' | |||
return self._is_unique | |||
@property | |||
def descriptive_name(self): | |||
'''str: A descriptive name for this CustomObject instance.''' | |||
# Specialization of ObservableObject's method to include the object type. | |||
return "%s id=%d" % (self.object_type.name, self.object_id) | |||
#### Private Event Handlers #### | |||
#### Public Event Handlers #### | |||
#### Commands #### | |||
class _CustomObjectType(collections.namedtuple('_CustomObjectType', 'name id')): | |||
# Tuple mapping between CLAD ActionResult name and ID | |||
# All instances will be members of ActionResults | |||
# Keep _ActionResult as lightweight as a normal namedtuple | |||
__slots__ = () | |||
def __str__(self): | |||
return 'CustomObjectTypes.%s' % self.name | |||
class CustomObjectTypes: | |||
'''Defines all available custom object types. | |||
For use with world.define_custom methods such as | |||
:meth:`cozmo.world.World.define_custom_box`, | |||
:meth:`cozmo.world.World.define_custom_cube`, and | |||
:meth:`cozmo.world.World.define_custom_wall` | |||
''' | |||
#: CustomType00 - the first custom object type | |||
CustomType00 = _CustomObjectType("CustomType00", _clad_to_engine_cozmo.ObjectType.CustomType00) | |||
#: | |||
CustomType01 = _CustomObjectType("CustomType01", _clad_to_engine_cozmo.ObjectType.CustomType01) | |||
#: | |||
CustomType02 = _CustomObjectType("CustomType02", _clad_to_engine_cozmo.ObjectType.CustomType02) | |||
#: | |||
CustomType03 = _CustomObjectType("CustomType03", _clad_to_engine_cozmo.ObjectType.CustomType03) | |||
#: | |||
CustomType04 = _CustomObjectType("CustomType04", _clad_to_engine_cozmo.ObjectType.CustomType04) | |||
#: | |||
CustomType05 = _CustomObjectType("CustomType05", _clad_to_engine_cozmo.ObjectType.CustomType05) | |||
#: | |||
CustomType06 = _CustomObjectType("CustomType06", _clad_to_engine_cozmo.ObjectType.CustomType06) | |||
#: | |||
CustomType07 = _CustomObjectType("CustomType07", _clad_to_engine_cozmo.ObjectType.CustomType07) | |||
#: | |||
CustomType08 = _CustomObjectType("CustomType08", _clad_to_engine_cozmo.ObjectType.CustomType08) | |||
#: | |||
CustomType09 = _CustomObjectType("CustomType09", _clad_to_engine_cozmo.ObjectType.CustomType09) | |||
#: | |||
CustomType10 = _CustomObjectType("CustomType10", _clad_to_engine_cozmo.ObjectType.CustomType10) | |||
#: | |||
CustomType11 = _CustomObjectType("CustomType11", _clad_to_engine_cozmo.ObjectType.CustomType11) | |||
#: | |||
CustomType12 = _CustomObjectType("CustomType12", _clad_to_engine_cozmo.ObjectType.CustomType12) | |||
#: | |||
CustomType13 = _CustomObjectType("CustomType13", _clad_to_engine_cozmo.ObjectType.CustomType13) | |||
#: | |||
CustomType14 = _CustomObjectType("CustomType14", _clad_to_engine_cozmo.ObjectType.CustomType14) | |||
#: | |||
CustomType15 = _CustomObjectType("CustomType15", _clad_to_engine_cozmo.ObjectType.CustomType15) | |||
#: | |||
CustomType16 = _CustomObjectType("CustomType16", _clad_to_engine_cozmo.ObjectType.CustomType16) | |||
#: | |||
CustomType17 = _CustomObjectType("CustomType17", _clad_to_engine_cozmo.ObjectType.CustomType17) | |||
#: | |||
CustomType18 = _CustomObjectType("CustomType18", _clad_to_engine_cozmo.ObjectType.CustomType18) | |||
#: CustomType19 - the last custom object type | |||
CustomType19 = _CustomObjectType("CustomType19", _clad_to_engine_cozmo.ObjectType.CustomType19) | |||
_CustomObjectMarker = collections.namedtuple('_CustomObjectMarker', 'name id') | |||
class CustomObjectMarkers: | |||
'''Defines all available custom object markers. | |||
For use with world.define_custom methods such as | |||
:meth:`cozmo.world.World.define_custom_box`, | |||
:meth:`cozmo.world.World.define_custom_cube`, and | |||
:meth:`cozmo.world.World.define_custom_wall` | |||
''' | |||
#: .. image:: ../images/custom_markers/SDK_2Circles.png | |||
Circles2 = _CustomObjectMarker("Circles2", _clad_to_engine_cozmo.CustomObjectMarker.Circles2) | |||
#: .. image:: ../images/custom_markers/SDK_3Circles.png | |||
Circles3 = _CustomObjectMarker("Circles3", _clad_to_engine_cozmo.CustomObjectMarker.Circles3) | |||
#: .. image:: ../images/custom_markers/SDK_4Circles.png | |||
Circles4 = _CustomObjectMarker("Circles4", _clad_to_engine_cozmo.CustomObjectMarker.Circles4) | |||
#: .. image:: ../images/custom_markers/SDK_5Circles.png | |||
Circles5 = _CustomObjectMarker("Circles5", _clad_to_engine_cozmo.CustomObjectMarker.Circles5) | |||
#: .. image:: ../images/custom_markers/SDK_2Diamonds.png | |||
Diamonds2 = _CustomObjectMarker("Diamonds2", _clad_to_engine_cozmo.CustomObjectMarker.Diamonds2) | |||
#: .. image:: ../images/custom_markers/SDK_3Diamonds.png | |||
Diamonds3 = _CustomObjectMarker("Diamonds3", _clad_to_engine_cozmo.CustomObjectMarker.Diamonds3) | |||
#: .. image:: ../images/custom_markers/SDK_4Diamonds.png | |||
Diamonds4 = _CustomObjectMarker("Diamonds4", _clad_to_engine_cozmo.CustomObjectMarker.Diamonds4) | |||
#: .. image:: ../images/custom_markers/SDK_5Diamonds.png | |||
Diamonds5 = _CustomObjectMarker("Diamonds5", _clad_to_engine_cozmo.CustomObjectMarker.Diamonds5) | |||
#: .. image:: ../images/custom_markers/SDK_2Hexagons.png | |||
Hexagons2 = _CustomObjectMarker("Hexagons2", _clad_to_engine_cozmo.CustomObjectMarker.Hexagons2) | |||
#: .. image:: ../images/custom_markers/SDK_3Hexagons.png | |||
Hexagons3 = _CustomObjectMarker("Hexagons3", _clad_to_engine_cozmo.CustomObjectMarker.Hexagons3) | |||
#: .. image:: ../images/custom_markers/SDK_4Hexagons.png | |||
Hexagons4 = _CustomObjectMarker("Hexagons4", _clad_to_engine_cozmo.CustomObjectMarker.Hexagons4) | |||
#: .. image:: ../images/custom_markers/SDK_5Hexagons.png | |||
Hexagons5 = _CustomObjectMarker("Hexagons5", _clad_to_engine_cozmo.CustomObjectMarker.Hexagons5) | |||
#: .. image:: ../images/custom_markers/SDK_2Triangles.png | |||
Triangles2 = _CustomObjectMarker("Triangles2", _clad_to_engine_cozmo.CustomObjectMarker.Triangles2) | |||
#: .. image:: ../images/custom_markers/SDK_3Triangles.png | |||
Triangles3 = _CustomObjectMarker("Triangles3", _clad_to_engine_cozmo.CustomObjectMarker.Triangles3) | |||
#: .. image:: ../images/custom_markers/SDK_4Triangles.png | |||
Triangles4 = _CustomObjectMarker("Triangles4", _clad_to_engine_cozmo.CustomObjectMarker.Triangles4) | |||
#: .. image:: ../images/custom_markers/SDK_5Triangles.png | |||
Triangles5 = _CustomObjectMarker("Triangles5", _clad_to_engine_cozmo.CustomObjectMarker.Triangles5) | |||
class FixedCustomObject(): | |||
'''A fixed object defined by the SDK. It is given a pose and x,y,z sizes. | |||
This object cannot be observed by the robot so its pose never changes. | |||
The position is static in Cozmo's world view; once instantiated, these | |||
objects never move. This could be used to make Cozmo aware of objects and | |||
know to plot a path around them even when they don't have any markers. | |||
To create these use :meth:`~cozmo.world.World.create_custom_fixed_object` | |||
''' | |||
is_visible = False | |||
def __init__(self, pose, x_size_mm, y_size_mm, z_size_mm, object_id, *a, **kw): | |||
super().__init__(*a, **kw) | |||
self._pose = pose | |||
self._object_id = object_id | |||
self._x_size_mm = x_size_mm | |||
self._y_size_mm = y_size_mm | |||
self._z_size_mm = z_size_mm | |||
def __repr__(self): | |||
return ('<%s pose=%s object_id=%d x_size_mm=%.1f y_size_mm=%.1f z_size_mm=%.1f=>' % | |||
(self.__class__.__name__, self.pose, self.object_id, | |||
self.x_size_mm, self.y_size_mm, self.z_size_mm)) | |||
#### Private Methods #### | |||
#### Event Wrappers #### | |||
#### Properties #### | |||
@property | |||
def object_id(self): | |||
'''int: The internal ID assigned to the object. | |||
This value can only be assigned once as it is static in the engine. | |||
''' | |||
return self._object_id | |||
@object_id.setter | |||
def object_id(self, value): | |||
if self._object_id is not None: | |||
raise ValueError("Cannot change object ID once set (from %s to %s)" % (self._object_id, value)) | |||
logger.debug("Updated object_id for %s from %s to %s", self.__class__, self._object_id, value) | |||
self._object_id = value | |||
@property | |||
def pose(self): | |||
''':class:`cozmo.util.Pose`: The pose of the object in the world.''' | |||
return self._pose | |||
@property | |||
def x_size_mm(self): | |||
'''float: The length of the object in its X axis, in millimeters.''' | |||
return self._x_size_mm | |||
@property | |||
def y_size_mm(self): | |||
'''float: The length of the object in its Y axis, in millimeters.''' | |||
return self._y_size_mm | |||
@property | |||
def z_size_mm(self): | |||
'''float: The length of the object in its Z axis, in millimeters.''' | |||
return self._z_size_mm | |||
#### Private Event Handlers #### | |||
#### Public Event Handlers #### | |||
#### Commands #### |
@@ -0,0 +1,133 @@ | |||
# Copyright (c) 2016 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. | |||
''' Cozmo's OLED screen that displays his face - related functions and values. | |||
''' | |||
# __all__ should order by constants, event classes, other classes, functions. | |||
__all__ = ['dimensions', 'convert_pixels_to_screen_data', | |||
'convert_image_to_screen_data'] | |||
SCREEN_WIDTH = 128 | |||
SCREEN_HALF_HEIGHT = 32 | |||
SCREEN_HEIGHT = SCREEN_HALF_HEIGHT * 2 | |||
def dimensions(): | |||
'''Return the dimension (width, height) of the oled screen. | |||
Note: The screen is displayed interlaced, with only every other line displayed | |||
This alternates every time the image is changed (no longer than 30 seconds) | |||
to prevent screen burn-in. Therefore to ensure the image looks correct on | |||
either scan-line offset we use half the vertical resolution | |||
Returns: | |||
A tuple of ints (width, height) | |||
''' | |||
return SCREEN_WIDTH, SCREEN_HALF_HEIGHT | |||
def convert_pixels_to_screen_data(pixel_data, image_width, image_height): | |||
'''Convert a sequence of pixel data to the correct format to display on Cozmo's face. | |||
Args: | |||
pixel_data (:class:`bytes`): sequence of pixel values, should be in binary (1s or 0s) | |||
image_width (int): width of the image defined by the pixel_data | |||
image_height (int): height of the image defined by the pixel_data | |||
Returns: | |||
A :class:`bytearray` object representing all of the pixels (8 pixels packed per byte) | |||
Raises: | |||
ValueError: Invalid Dimensions | |||
ValueError: Bad image_width | |||
ValueError: Bad image_height | |||
''' | |||
if len(pixel_data) != (image_width * image_height): | |||
raise ValueError('Invalid Dimensions: len(pixel_data) {0} != image_width={1} * image_height={2} (== {3})'. | |||
format(len(pixel_data), image_width, image_height, image_width * image_height)) | |||
num_columns_per_pixel = int(SCREEN_WIDTH / image_width) | |||
num_rows_per_pixel = int(SCREEN_HEIGHT / image_height) | |||
if (image_width * num_columns_per_pixel) != SCREEN_WIDTH: | |||
raise ValueError('Bad image_width: image_width {0} must be an exact integer divisor of {1}'. | |||
format(image_width, SCREEN_WIDTH)) | |||
if (image_height * num_rows_per_pixel) != SCREEN_HEIGHT: | |||
raise ValueError('Bad image_height: image_height {0} must be an exact integer divisor of {1}'. | |||
format(image_height, SCREEN_HEIGHT)) | |||
pixel_chunks = zip(*[iter(pixel_data)] * 8) # convert into 8 pixel chunks - we'll pack each as 1 byte later | |||
pixel_chunks_per_row = int(SCREEN_WIDTH / 8) # 8 pixels per byte (pixel-chunk) | |||
result_bytes = bytearray() | |||
x = 0 | |||
y = 0 | |||
for pixel_chunk in pixel_chunks: | |||
# convert the 8 pixels in the chunk into bits to write out | |||
# write each pixel bit num_columns_per_pixel times in a row | |||
pixel_byte = 0 | |||
for pixel in pixel_chunk: | |||
for _ in range(num_columns_per_pixel): | |||
pixel_byte <<= 1 | |||
pixel_byte += pixel | |||
x += 1 | |||
if (x % 8) == 0: | |||
result_bytes.append(pixel_byte) | |||
pixel_byte = 0 | |||
# check if this is the end of a row | |||
if x == SCREEN_WIDTH: | |||
x = 0 | |||
y += 1 | |||
if (x == 0) and (num_rows_per_pixel > 0): | |||
# at the end of a row - copy that row for every extra row-per-pixel | |||
for _ in range(num_rows_per_pixel-1): | |||
start_of_last_row = len(result_bytes) - pixel_chunks_per_row | |||
result_bytes.extend(result_bytes[start_of_last_row:]) | |||
return result_bytes | |||
def convert_image_to_screen_data(image, invert_image=False, pixel_threshold=127): | |||
''' Convert an image into the correct format to display on Cozmo's face. | |||
Args: | |||
image (:class:`~PIL.Image.Image`): The image to display on Cozmo's face | |||
invert_image (bool): If true then pixels darker than the threshold are set on | |||
pixel_threshold (int): The grayscale threshold for what to consider on or off (0..255) | |||
Returns: | |||
A :class:`bytearray` object representing all of the pixels (8 pixels packed per byte) | |||
''' | |||
# convert to grayscale | |||
grayscale_image = image.convert('L') | |||
# convert to binary white/black (1/0) | |||
if invert_image: | |||
def pixel_func(x): return 1 if x <= pixel_threshold else 0 | |||
else: | |||
def pixel_func(x): return 1 if x >= pixel_threshold else 0 | |||
bw = grayscale_image.point(pixel_func, '1') | |||
# convert to a flattened 1D bytes object of pixel values (1s or 0s in this case) | |||
pixel_data = bytes(bw.getdata()) | |||
return convert_pixels_to_screen_data(pixel_data, image.width, image.height) |
@@ -0,0 +1,181 @@ | |||
# Copyright (c) 2016 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. | |||
'''Pet detection. | |||
Cozmo is capable of detecting pet faces (cats and dogs). | |||
The :class:`cozmo.world.World` object keeps track of pets the robot currently | |||
knows about, along with those that are currently visible to the camera. | |||
Each pet is assigned a :class:`Pet` object, which generates a number of | |||
observable events whenever the pet is observed, etc. | |||
If a pet goes off-screen, it will be assigned a new object_id (and | |||
therefore a new Pet object will be created) when it returns. | |||
This is because the system can only tell if something appears to be | |||
a cat or a dog; it cannot recognize a specific pet or, for instance, | |||
tell the difference between two dogs. | |||
Note that these pet-specific events are also passed up to the | |||
:class:`cozmo.world.World` object, so events for all pets can be | |||
observed by adding handlers there. | |||
''' | |||
# __all__ should order by constants, event classes, other classes, functions. | |||
__all__ = ['PET_VISIBILITY_TIMEOUT', 'PET_TYPE_CAT', 'PET_TYPE_DOG', 'PET_TYPE_UNKNOWN', | |||
'EvtPetAppeared', 'EvtPetDisappeared', 'EvtPetObserved', | |||
'Pet'] | |||
import math | |||
import time | |||
from . import logger | |||
from . import event | |||
from . import objects | |||
from . import util | |||
from ._clad import _clad_to_game_anki | |||
#: Length of time in seconds to go without receiving an observed event before | |||
#: assuming that Cozmo can no longer see a pet. | |||
PET_VISIBILITY_TIMEOUT = objects.OBJECT_VISIBILITY_TIMEOUT | |||
# Pet types that Cozmo can distinguish | |||
#: Pet Type reported by Cozmo when unsure of type of pet | |||
PET_TYPE_UNKNOWN = "unknown" | |||
#: Pet Type reported by Cozmo when he thinks it's a cat | |||
PET_TYPE_CAT = "cat" | |||
#: Pet Type reported by Cozmo when he thinks it's a dog | |||
PET_TYPE_DOG = "dog" | |||
class EvtPetObserved(event.Event): | |||
'''Triggered whenever a pet is visually identified by the robot. | |||
A stream of these events are produced while a pet is visible to the robot. | |||
Each event has an updated image_box field. | |||
See EvtPetAppeared if you only want to know when a pet first | |||
becomes visible. | |||
''' | |||
pet = 'The Pet instance that was observed' | |||
updated = 'A set of field names that have changed' | |||
image_box = 'A comzo.util.ImageBox defining where the pet is within Cozmo\'s camera view' | |||
class EvtPetAppeared(event.Event): | |||
'''Triggered whenever a pet is first visually identified by a robot. | |||
This differs from EvtPetObserved in that it's only triggered when | |||
a pet initially becomes visible. If it disappears for more than | |||
PET_VISIBILITY_TIMEOUT seconds and then is seen again, a | |||
EvtPetDisappeared will be dispatched, followed by another | |||
EvtPetAppeared event. | |||
For continuous tracking information about a visible pet, see | |||
EvtPetObserved. | |||
''' | |||
pet = 'The Pet instance that was observed' | |||
updated = 'A set of field names that have changed' | |||
image_box = 'A comzo.util.ImageBox defining where the pet is within Cozmo\'s camera view' | |||
class EvtPetDisappeared(event.Event): | |||
'''Triggered whenever a pet that was previously being observed is no longer visible.''' | |||
pet = 'The Pet instance that is no longer being observed' | |||
def _clad_pet_type_to_pet_type(clad_pet_type): | |||
if clad_pet_type == _clad_to_game_anki.Vision.PetType.Unknown: | |||
return PET_TYPE_UNKNOWN | |||
elif clad_pet_type == _clad_to_game_anki.Vision.PetType.Cat: | |||
return PET_TYPE_CAT | |||
elif clad_pet_type == _clad_to_game_anki.Vision.PetType.Dog: | |||
return PET_TYPE_DOG | |||
else: | |||
raise ValueError("Unexpected pet type %s" % clad_pet_type) | |||
class Pet(objects.ObservableElement): | |||
'''A single pet that Cozmo has detected. | |||
See parent class :class:`~cozmo.objects.ObservableElement` for additional properties | |||
and methods. | |||
''' | |||
#: Length of time in seconds to go without receiving an observed event before | |||
#: assuming that Cozmo can no longer see a pet. | |||
visibility_timeout = PET_VISIBILITY_TIMEOUT | |||
def __init__(self, conn, world, robot, pet_id=None, **kw): | |||
super().__init__(conn, world, robot, **kw) | |||
self._pet_id = pet_id | |||
#: The type of Pet (PET_TYPE_CAT, PET_TYPE_DOG or PET_TYPE_UNKNOWN) | |||
self.pet_type = None | |||
def _repr_values(self): | |||
return 'pet_id=%s pet_type=%s' % (self.pet_id, self.pet_type) | |||
#### Private Methods #### | |||
def _dispatch_observed_event(self, changed_fields, image_box): | |||
self.dispatch_event(EvtPetObserved, pet=self, | |||
updated=changed_fields, image_box=image_box) | |||
def _dispatch_appeared_event(self, changed_fields, image_box): | |||
self.dispatch_event(EvtPetAppeared, pet=self, | |||
updated=changed_fields, image_box=image_box) | |||
def _dispatch_disappeared_event(self): | |||
self.dispatch_event(EvtPetDisappeared, pet=self) | |||
#### Properties #### | |||
@property | |||
def pet_id(self): | |||
'''int: The internal ID assigned to the pet. | |||
This value can only be assigned once as it is static in the engine. | |||
''' | |||
return self._pet_id | |||
@pet_id.setter | |||
def pet_id(self, value): | |||
if self._pet_id is not None: | |||
raise ValueError("Cannot change pet ID once set (from %s to %s)" % (self._pet_id, value)) | |||
logger.debug("Updated pet_id for %s from %s to %s", self.__class__, self._pet_id, value) | |||
self._pet_id = value | |||
#### Private Event Handlers #### | |||
def _recv_msg_robot_observed_pet(self, evt, *, msg): | |||
changed_fields = set() | |||
pet_type = _clad_pet_type_to_pet_type(msg.petType) | |||
if pet_type != self.pet_type: | |||
self.pet_type = pet_type | |||
changed_fields.add('pet_type') | |||
image_box = util.ImageBox._create_from_clad_rect(msg.img_rect) | |||
self._on_observed(image_box, msg.timestamp, changed_fields) | |||
#### Public Event Handlers #### | |||
#### Event Wrappers #### | |||
#### Commands #### |
@@ -0,0 +1,49 @@ | |||
# Copyright (c) 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. | |||
''' | |||
RobotAlignment related classes, functions, events and values. | |||
''' | |||
# __all__ should order by constants, event classes, other classes, functions. | |||
__all__ = ['RobotAlignmentTypes'] | |||
import collections | |||
from ._clad import _clad_to_engine_cozmo, CladEnumWrapper | |||
_RobotAlignmentType = collections.namedtuple('_RobotAlignmentType', ['name', 'id']) | |||
class RobotAlignmentTypes(CladEnumWrapper): | |||
'''Defines all robot alignment types. | |||
''' | |||
_clad_enum = _clad_to_engine_cozmo.AlignmentType | |||
_entry_type = _RobotAlignmentType | |||
#: Align the tips of the lift fingers with the target object | |||
LiftFinger = _entry_type("LiftFinger", _clad_enum.LIFT_FINGER) | |||
#: Align the flat part of the lift with the object | |||
#: (Useful for getting the fingers in the cube's grooves) | |||
LiftPlate = _entry_type("LiftPlate", _clad_enum.LIFT_PLATE) | |||
#: Align the front of cozmo's body | |||
#: (Useful for when the lift is up) | |||
Body = _entry_type("Body", _clad_enum.BODY) | |||
#: For use with distanceFromMarker parameter | |||
Custom = _entry_type("Custom", _clad_enum.CUSTOM) | |||
RobotAlignmentTypes._init_class() |
@@ -0,0 +1,857 @@ | |||
# 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. | |||
'''The run module contains helper classes and functions for opening a connection to the engine. | |||
To get started, the :func:`run_program` function can be used for most cases, | |||
it handles connecting to a device and then running the function you provide with | |||
the SDK-provided Robot object passed in. | |||
The :func:`connect` function can be used to open a connection | |||
and run your own code connected to a :class:`cozmo.conn.CozmoConnection` | |||
instance. It takes care of setting up an event loop, finding the Android or | |||
iOS device running the Cozmo app and making sure the connection is ok. | |||
You can also use the :func:`connect_with_tkviewer` or :func:`connect_with_3dviewer` | |||
functions which works in a similar way to :func:`connect`, but will also display | |||
either a a window on the screen showing a view from Cozmo's camera (using Tk), or | |||
a 3d viewer (with optional 2nd window showing Cozmo's camera) (using OpenGL), if | |||
supported on your system. | |||
Finally, more advanced progarms can integrate the SDK with an existing event | |||
loop by using the :func:`connect_on_loop` function. | |||
All of these functions make use of a :class:`DeviceConnector` subclass to | |||
deal with actually connecting to an Android or iOS device. There shouldn't | |||
normally be a need to modify them or write your own. | |||
''' | |||
# __all__ should order by constants, event classes, other classes, functions. | |||
__all__ = ['DeviceConnector', 'IOSConnector', 'AndroidConnector', 'TCPConnector', | |||
'connect', 'connect_with_3dviewer', 'connect_with_tkviewer', 'connect_on_loop', | |||
'run_program', 'setup_basic_logging'] | |||
import threading | |||
import asyncio | |||
import concurrent.futures | |||
import functools | |||
import inspect | |||
import logging | |||
import os | |||
import os.path | |||
import queue | |||
import shutil | |||
import subprocess | |||
import sys | |||
import types | |||
import warnings | |||
from . import logger, logger_protocol | |||
from . import base | |||
from . import clad_protocol | |||
from . import conn | |||
from . import event | |||
from . import exceptions | |||
from . import usbmux | |||
#: The TCP port number we expect the Cozmo app to be listening on. | |||
COZMO_PORT = 5106 | |||
if sys.platform in ('win32', 'cygwin'): | |||
DEFAULT_ADB_CMD = 'adb.exe' | |||
else: | |||
DEFAULT_ADB_CMD = 'adb' | |||
def _observe_connection_lost(proto, cb): | |||
meth = proto.connection_lost | |||
@functools.wraps(meth) | |||
def connection_lost(self, exc): | |||
meth(exc) | |||
cb() | |||
proto.connection_lost = types.MethodType(connection_lost, proto) | |||
class DeviceConnector: | |||
'''Base class for objects that setup the physical connection to a device.''' | |||
def __init__(self, cozmo_port=COZMO_PORT, enable_env_vars=True): | |||
self.cozmo_port = cozmo_port | |||
if enable_env_vars: | |||
self.parse_env_vars() | |||
async def connect(self, loop, protocol_factory, conn_check): | |||
'''Connect attempts to open a connection transport to the Cozmo app on a device. | |||
On opening a transport it will create a protocol from the supplied | |||
factory and connect it to the transport, returning a (transport, protocol) | |||
tuple. See :meth:`asyncio.BaseEventLoop.create_connection` | |||
''' | |||
raise NotImplementedError | |||
def parse_env_vars(self): | |||
try: | |||
self.cozmo_port = int(os.environ['COZMO_PORT']) | |||
except (KeyError, ValueError): | |||
pass | |||
class IOSConnector(DeviceConnector): | |||
'''Connects to an attached iOS device over USB. | |||
Opens a connection to the first iOS device that's found to be running | |||
the Cozmo app in SDK mode. | |||
iTunes (or another service providing usbmuxd) must be installed in order | |||
for this connector to be able to open a connection to a device. | |||
An instance of this class can be passed to the ``connect_`` prefixed | |||
functions in this module. | |||
Args: | |||
serial (string): Serial number of the device to connect to. | |||
If None, then connect to the first available iOS device running | |||
the Cozmo app in SDK mode. | |||
''' | |||
def __init__(self, serial=None, **kw): | |||
super().__init__(**kw) | |||
self.usbmux = None | |||
self._connected = set() | |||
self.serial = serial | |||
async def connect(self, loop, protocol_factory, conn_check): | |||
if not self.usbmux: | |||
self.usbmux = await usbmux.connect_to_usbmux(loop=loop) | |||
try: | |||
if self.serial is None: | |||
device_info, transport, proto = await self.usbmux.connect_to_first_device( | |||
protocol_factory, self.cozmo_port, exclude=self._connected) | |||
else: | |||
device_id = await self.usbmux.wait_for_serial(self.serial) | |||
device_info, transport, proto = await self.usbmux.connect_to_device( | |||
protocol_factory, device_id, self.cozmo_port) | |||
except asyncio.TimeoutError as exc: | |||
raise exceptions.ConnectionError("No connected iOS devices running Cozmo in SDK mode") from exc | |||
device_id = device_info.get('DeviceID') | |||
proto.device_info={ | |||
'device_type': 'ios', | |||
'device_id': device_id, | |||
'serial': device_info.get('SerialNumber') | |||
} | |||
if conn_check is not None: | |||
await conn_check(proto) | |||
self._connected.add(device_id) | |||
logger.info('Connected to iOS device_id=%s serial=%s', device_id, | |||
device_info.get('SerialNumber')) | |||
_observe_connection_lost(proto, functools.partial(self._disconnect, device_id)) | |||
return transport, proto | |||
def _disconnect(self, device_id): | |||
logger.info('iOS device_id=%s disconnected.', device_id) | |||
self._connected.discard(device_id) | |||
class AndroidConnector(DeviceConnector): | |||
'''Connects to an attached Android device over USB. | |||
This requires the Android Studio command line tools to be installed, | |||
specifically `adb`. | |||
By default the connector will attempt to locate `adb` (or `adb.exe` | |||
on Windows) in common locations, but it may also be supplied by setting | |||
the ``ANDROID_ADB_PATH`` environment variable, or by passing it | |||
to the constructor. | |||
An instance of this class can be passed to the ``connect_`` prefixed | |||
functions in this module. | |||
Args: | |||
serial (string): Serial number of the device to connect to. | |||
If None, then connect to the first available Android device running | |||
the Cozmo app in SDK mode. | |||
''' | |||
def __init__(self, adb_cmd=None, serial=None, **kw): | |||
self._adb_cmd = None | |||
super().__init__(**kw) | |||
self.serial = serial | |||
self.portspec = 'tcp:' + str(self.cozmo_port) | |||
self._connected = set() | |||
if adb_cmd: | |||
self._adb_cmd = adb_cmd | |||
else: | |||
self._adb_cmd = shutil.which(DEFAULT_ADB_CMD) | |||
def parse_env_vars(self): | |||
super().parse_env_vars() | |||
self._adb_cmd = os.environ.get('ANDROID_ADB_PATH') | |||
@property | |||
def adb_cmd(self): | |||
if self._adb_cmd is not None: | |||
return self._adb_cmd | |||
if sys.platform != 'win32': | |||
return DEFAULT_ADB_CMD | |||
# C:\Users\IEUser\AppData\Local\Android\android-sdk | |||
# C:\Program Files (x86)\Android\android-sdk | |||
try_paths = [] | |||
for path in [os.environ[key] for key in ('LOCALAPPDATA', 'ProgramFiles', 'ProgramFiles(x86)') if key in os.environ]: | |||
try_paths.append(os.path.join(path, 'Android', 'android-sdk')) | |||
for path in try_paths: | |||
adb_path = os.path.join(path, 'platform-tools', 'adb.exe') | |||
if os.path.exists(adb_path): | |||
self._adb_cmd = adb_path | |||
logger.debug('Found adb.exe at %s', adb_path) | |||
return adb_path | |||
raise ValueError('Could not find Android development tools') | |||
def _exec(self, *args): | |||
try: | |||
result = subprocess.run([self.adb_cmd] + list(args), | |||
stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=5) | |||
except Exception as e: | |||
raise ValueError('Failed to execute adb command %s: %s' % (self.adb_cmd, e)) | |||
if result.returncode != 0: | |||
raise ValueError('Failed to execute adb command %s: %s' % (result.args, result.stderr)) | |||
return result.stdout.split(b'\n') | |||
def _devices(self): | |||
for line in self._exec('devices'): | |||
line = line.split() | |||
if len(line) != 2 or line[1] != b'device': | |||
continue | |||
yield line[0].decode('ascii') # device serial # | |||
def _add_forward(self, serial): | |||
self._exec('-s', serial, 'forward', self.portspec, self.portspec) | |||
def _remove_forward(self, serial): | |||
self._exec('-s', serial, 'forward', '--remove', self.portspec) | |||
async def connect(self, loop, protocol_factory, conn_check): | |||
version_mismatch = None | |||
for serial in self._devices(): | |||
if serial in self._connected: | |||
continue | |||
if self.serial is not None and serial.lower() != self.serial.lower(): | |||
continue | |||
logger.debug('Checking connection to Android device: %s', serial) | |||
try: | |||
self._remove_forward(serial) | |||
except: | |||
pass | |||
self._add_forward(serial) | |||
try: | |||
transport, proto = await loop.create_connection( | |||
protocol_factory, '127.0.0.1', self.cozmo_port) | |||
proto.device_info={ | |||
'device_type': 'android', | |||
'serial': serial, | |||
} | |||
if conn_check: | |||
# Check that we have a good connection before returning | |||
try: | |||
await conn_check(proto) | |||
except Exception as e: | |||
logger.debug('Failed connection check: %s', e) | |||
raise | |||
logger.info('Connected to Android device serial=%s', serial) | |||
self._connected.add(serial) | |||
_observe_connection_lost(proto, functools.partial(self._disconnect, serial)) | |||
return transport, proto | |||
except exceptions.SDKVersionMismatch as e: | |||
version_mismatch = e | |||
except: | |||
pass | |||
self._remove_forward(serial) | |||
if version_mismatch is not None: | |||
raise version_mismatch | |||
raise exceptions.ConnectionError("No connected Android devices running Cozmo in SDK mode") | |||
def _disconnect(self, serial): | |||
logger.info('Android serial=%s disconnected.', serial) | |||
self._connected.discard(serial) | |||
class TCPConnector(DeviceConnector): | |||
'''Connects to the Cozmo app directly via TCP. | |||
Generally only used for testing and debugging. | |||
Requires that a SDK_TCP_PORT environment variable be set to the port | |||
number to connect to. | |||
''' | |||
def __init__(self, tcp_port=None, ip_addr='127.0.0.1', **kw): | |||
super().__init__(**kw) | |||
self.ip_addr = ip_addr | |||
if tcp_port is not None: | |||
# override SDK_TCP_PORT environment variable | |||
self.tcp_port = tcp_port | |||
def parse_env_vars(self): | |||
super().parse_env_vars() | |||
self.tcp_port = None | |||
try: | |||
self.tcp_port = int(os.environ['SDK_TCP_PORT']) | |||
except (KeyError, ValueError): | |||
pass | |||
@property | |||
def enabled(self): | |||
return self.tcp_port is not None | |||
async def connect(self, loop, protocol_factory, conn_check): | |||
transport, proto = await loop.create_connection(protocol_factory, self.ip_addr, self.tcp_port) | |||
proto.device_info={ | |||
'device_type': 'tcp', | |||
'host': '%s:%s' % (self.ip_addr, self.tcp_port), | |||
} | |||
if conn_check: | |||
try: | |||
await conn_check(proto) | |||
except Exception as e: | |||
logger.debug('Failed connection check: %s', e) | |||
raise | |||
logger.info("Connected to device on TCP port %d" % self.tcp_port) | |||
return transport, proto | |||
class FirstAvailableConnector(DeviceConnector): | |||
'''Connects to the first Android or iOS device running the Cozmo app in SDK mode. | |||
This class creates an :class:`AndroidConnector` or :class:`IOSConnector` | |||
instance and returns the first successful connection. | |||
This is the default connector used by ``connect_`` functions. | |||
''' | |||
def __init__(self): | |||
super().__init__(self, enable_env_vars=False) | |||
self.tcp = TCPConnector() | |||
self.ios = IOSConnector() | |||
self.android = AndroidConnector() | |||
async def _do_connect(self, connector,loop, protocol_factory, conn_check): | |||
connect = connector.connect(loop, protocol_factory, conn_check) | |||
result = await asyncio.gather(connect, loop=loop, return_exceptions=True) | |||
return result[0] | |||
async def connect(self, loop, protocol_factory, conn_check): | |||
conn_args = (loop, protocol_factory, conn_check) | |||
tcp_result = None | |||
if self.tcp.enabled: | |||
tcp_result = await self._do_connect(self.tcp, *conn_args) | |||
if not isinstance(tcp_result, BaseException): | |||
return tcp_result | |||
logger.warning('No TCP connection found running Cozmo: %s', tcp_result) | |||
android_result = await self._do_connect(self.android, *conn_args) | |||
if not isinstance(android_result, BaseException): | |||
return android_result | |||
ios_result = await self._do_connect(self.ios, *conn_args) | |||
if not isinstance(ios_result, BaseException): | |||
return ios_result | |||
logger.warning('No iOS device found running Cozmo: %s', ios_result) | |||
logger.warning('No Android device found running Cozmo: %s', android_result) | |||
if isinstance(tcp_result, exceptions.SDKVersionMismatch): | |||
raise tcp_result | |||
if isinstance(ios_result, exceptions.SDKVersionMismatch): | |||
raise ios_result | |||
if isinstance(android_result, exceptions.SDKVersionMismatch): | |||
raise android_result | |||
raise exceptions.NoDevicesFound('No devices connected running Cozmo in SDK mode') | |||
# Create an instance of a connector to use by default | |||
# The instance will maintain state about which devices are currently connected. | |||
_DEFAULT_CONNECTOR = FirstAvailableConnector() | |||
def _sync_exception_handler(abort_future, loop, context): | |||
loop.default_exception_handler(context) | |||
exception = context.get('exception') | |||
if exception is not None: | |||
abort_future.set_exception(context['exception']) | |||
else: | |||
abort_future.set_exception(RuntimeError(context['message'])) | |||
class _LoopThread: | |||
'''Takes care of managing an event loop running in a dedicated thread. | |||
Args: | |||
loop (:class:`asyncio.BaseEventLoop`): The loop to run | |||
f (callable): Optional code to execute on the loop's thread | |||
conn_factory (callable): Override the factory function to generate a | |||
:class:`cozmo.conn.CozmoConnection` (or subclass) instance. | |||
connector (:class:`DeviceConnector`): Optional instance of a DeviceConnector | |||
subclass that handles opening the USB connection to a device. | |||
By default, it will connect to the first Android or iOS device that | |||
has the Cozmo app running in SDK mode. | |||
abort_future (:class:`concurrent.futures.Future): Optional future to | |||
raise an exception on in the event of an exception occurring within | |||
the thread. | |||
''' | |||
def __init__(self, loop, f=None, conn_factory=conn.CozmoConnection, connector=None, abort_future=None): | |||
self.loop = loop | |||
self.f = f | |||
if not abort_future: | |||
abort_future = concurrent.futures.Future() | |||
self.abort_future = abort_future | |||
self.conn_factory = conn_factory | |||
self.connector = connector | |||
self.thread = None | |||
self._running = False | |||
def start(self): | |||
'''Start a thread and open a connection to a device. | |||
Returns: | |||
:class:`cozmo.conn.CozmoConnection` instance | |||
''' | |||
q = queue.Queue() | |||
abort_future = concurrent.futures.Future() | |||
def run_loop(): | |||
asyncio.set_event_loop(self.loop) | |||
try: | |||
coz_conn = connect_on_loop(self.loop, self.conn_factory, self.connector) | |||
q.put(coz_conn) | |||
except Exception as e: | |||
self.abort_future.set_exception(e) | |||
q.put(e) | |||
return | |||
if self.f: | |||
asyncio.ensure_future(self.f(coz_conn)) | |||
self.loop.run_forever() | |||
self.thread = threading.Thread(target=run_loop) | |||
self.thread.start() | |||
coz_conn = q.get(10) | |||
if coz_conn is None: | |||
raise TimeoutError("Timed out waiting for connection to device") | |||
if isinstance(coz_conn, Exception): | |||
raise coz_conn | |||
self.coz_conn = coz_conn | |||
self._running = True | |||
return coz_conn | |||
def stop(self): | |||
'''Cleaning shutdown the running loop and thread.''' | |||
if self._running: | |||
async def _stop(): | |||
await self.coz_conn.shutdown() | |||
self.loop.call_soon(lambda: self.loop.stop()) | |||
asyncio.run_coroutine_threadsafe(_stop(), self.loop).result() | |||
self.thread.join() | |||
self._running = False | |||
def abort(self, exc): | |||
'''Abort the running loop and thread.''' | |||
if self._running: | |||
async def _abort(exc): | |||
self.coz_conn.abort(exc) | |||
asyncio.run_coroutine_threadsafe(_abort(exc), self.loop).result() | |||
self.stop() | |||
def _connect_async(f, conn_factory=conn.CozmoConnection, connector=None): | |||
# use the default loop, if one is available for the current thread, | |||
# if not create a new loop and make it the default. | |||
# | |||
# the expectation is that if the user wants explicit control over which | |||
# loop the code is executed on, they'll just use connect_on_loop directly. | |||
loop = None | |||
try: | |||
loop = asyncio.get_event_loop() | |||
except: | |||
pass | |||
if loop is None: | |||
loop = asyncio.new_event_loop() | |||
asyncio.set_event_loop(loop) | |||
coz_conn = connect_on_loop(loop, conn_factory, connector) | |||
try: | |||
loop.run_until_complete(f(coz_conn)) | |||
except KeyboardInterrupt: | |||
logger.info('Exit requested by user') | |||
finally: | |||
loop.run_until_complete(coz_conn.shutdown()) | |||
loop.stop() | |||
loop.run_forever() | |||
_sync_loop = asyncio.new_event_loop() | |||
def _connect_sync(f, conn_factory=conn.CozmoConnection, connector=None): | |||
abort_future = concurrent.futures.Future() | |||
conn_factory = functools.partial(conn_factory, _sync_abort_future=abort_future) | |||
lt = _LoopThread(_sync_loop, conn_factory=conn_factory, connector=connector, abort_future=abort_future) | |||
_sync_loop.set_exception_handler(functools.partial(_sync_exception_handler, abort_future)) | |||
coz_conn = lt.start() | |||
try: | |||
f(base._SyncProxy(coz_conn)) | |||
finally: | |||
lt.stop() | |||
def connect_on_loop(loop, conn_factory=conn.CozmoConnection, connector=None): | |||
'''Uses the supplied event loop to connect to a device. | |||
Will run the event loop in the current thread until the | |||
connection succeeds or fails. | |||
If you do not want/need to manage your own loop, then use the | |||
:func:`connect` function to handle setup/teardown and execute | |||
a user-supplied function. | |||
Args: | |||
loop (:class:`asyncio.BaseEventLoop`): The event loop to use to | |||
connect to Cozmo. | |||
conn_factory (callable): Override the factory function to generate a | |||
:class:`cozmo.conn.CozmoConnection` (or subclass) instance. | |||
connector (:class:`DeviceConnector`): Optional instance of a DeviceConnector | |||
subclass that handles opening the USB connection to a device. | |||
By default, it will connect to the first Android or iOS device that | |||
has the Cozmo app running in SDK mode. | |||
Returns: | |||
A :class:`cozmo.conn.CozmoConnection` instance. | |||
''' | |||
if connector is None: | |||
connector = _DEFAULT_CONNECTOR | |||
factory = functools.partial(conn_factory, loop=loop) | |||
async def conn_check(coz_conn): | |||
await coz_conn.wait_for(conn.EvtConnected, timeout=5) | |||
async def connect(): | |||
return await connector.connect(loop, factory, conn_check) | |||
transport, coz_conn = loop.run_until_complete(connect()) | |||
return coz_conn | |||
def connect(f, conn_factory=conn.CozmoConnection, connector=None): | |||
'''Connects to the Cozmo Engine on the mobile device and supplies the connection to a function. | |||
Accepts a function, f, that is given a :class:`cozmo.conn.CozmoConnection` object as | |||
a parameter. | |||
The supplied function may be either an asynchronous coroutine function | |||
(normally defined using ``async def``) or a regular synchronous function. | |||
If an asynchronous function is supplied it will be run on the same thread | |||
as the Cozmo event loop and must use the ``await`` keyword to yield control | |||
back to the loop. | |||
If a synchronous function is supplied then it will run on the main thread | |||
and Cozmo's event loop will run on a separate thread. Calls to | |||
asynchronous methods returned from CozmoConnection will automatically | |||
be translated to synchronous ones. | |||
The connect function will return once the supplied function has completed, | |||
as which time it will terminate the connection to the robot. | |||
Args: | |||
f (callable): The function to execute | |||
conn_factory (callable): Override the factory function to generate a | |||
:class:`cozmo.conn.CozmoConnection` (or subclass) instance. | |||
connector (:class:`DeviceConnector`): Optional instance of a DeviceConnector | |||
subclass that handles opening the USB connection to a device. | |||
By default it will connect to the first Android or iOS device that | |||
has the Cozmo app running in SDK mode. | |||
''' | |||
if asyncio.iscoroutinefunction(f): | |||
return _connect_async(f, conn_factory, connector) | |||
return _connect_sync(f, conn_factory, connector) | |||
def _connect_viewer(f, conn_factory, connector, viewer): | |||
# Run the viewer in the main thread, with the SDK running on a new background thread. | |||
loop = asyncio.new_event_loop() | |||
abort_future = concurrent.futures.Future() | |||
async def view_connector(coz_conn): | |||
try: | |||
await viewer.connect(coz_conn) | |||
if inspect.iscoroutinefunction(f): | |||
await f(coz_conn) | |||
else: | |||
await coz_conn._loop.run_in_executor(None, f, base._SyncProxy(coz_conn)) | |||
finally: | |||
viewer.disconnect() | |||
try: | |||
if not inspect.iscoroutinefunction(f): | |||
conn_factory = functools.partial(conn_factory, _sync_abort_future=abort_future) | |||
lt = _LoopThread(loop, f=view_connector, conn_factory=conn_factory, connector=connector) | |||
lt.start() | |||
viewer.mainloop() | |||
except BaseException as e: | |||
abort_future.set_exception(exceptions.SDKShutdown(repr(e))) | |||
raise | |||
finally: | |||
lt.stop() | |||
def connect_with_3dviewer(f, conn_factory=conn.CozmoConnection, connector=None, | |||
enable_camera_view=False, show_viewer_controls=True): | |||
'''Setup a connection to a device and run a user function while displaying Cozmo's 3d world. | |||
This displays an OpenGL window on the screen with a 3D view of Cozmo's | |||
understanding of the world. Optionally, if `use_viewer` is True, a 2nd OpenGL | |||
window will also display showing a view of Cozmo's camera. It will return an | |||
error if the current system does not support PyOpenGL. | |||
The function may be either synchronous or asynchronous (defined | |||
used ``async def``). | |||
The function must accept a :class:`cozmo.CozmoConnection` object as | |||
its only argument. | |||
This call will block until the supplied function completes. | |||
Args: | |||
f (callable): The function to execute | |||
conn_factory (callable): Override the factory function to generate a | |||
:class:`cozmo.conn.CozmoConnection` (or subclass) instance. | |||
connector (:class:`DeviceConnector`): Optional instance of a DeviceConnector | |||
subclass that handles opening the USB connection to a device. | |||
By default it will connect to the first Android or iOS device that | |||
has the Cozmo app running in SDK mode. | |||
enable_camera_view (bool): Specifies whether to also open a 2D camera | |||
view in a second OpenGL window. | |||
show_viewer_controls (bool): Specifies whether to draw controls on the view. | |||
''' | |||
try: | |||
from . import opengl | |||
except ImportError as exc: | |||
opengl = exc | |||
if isinstance(opengl, Exception): | |||
if isinstance(opengl, exceptions.InvalidOpenGLGlutImplementation): | |||
raise NotImplementedError('GLUT (OpenGL Utility Toolkit) is not available:\n%s' | |||
% opengl) | |||
else: | |||
raise NotImplementedError('opengl is not available; ' | |||
'make sure the PyOpenGL and Pillow packages are installed:\n' | |||
'Do `pip3 install --user cozmo[3dviewer]` to install. Error: %s' % opengl) | |||
viewer = opengl.OpenGLViewer(enable_camera_view=enable_camera_view, show_viewer_controls=show_viewer_controls) | |||
_connect_viewer(f, conn_factory, connector, viewer) | |||
def connect_with_tkviewer(f, conn_factory=conn.CozmoConnection, connector=None, force_on_top=False): | |||
'''Setup a connection to a device and run a user function while displaying Cozmo's camera. | |||
This displays a Tk window on the screen showing a view of Cozmo's camera. | |||
It will return an error if the current system does not support Tk. | |||
The function may be either synchronous or asynchronous (defined | |||
used ``async def``). | |||
The function must accept a :class:`cozmo.CozmoConnection` object as | |||
its only argument. | |||
This call will block until the supplied function completes. | |||
Args: | |||
f (callable): The function to execute | |||
conn_factory (callable): Override the factory function to generate a | |||
:class:`cozmo.conn.CozmoConnection` (or subclass) instance. | |||
connector (:class:`DeviceConnector`): Optional instance of a DeviceConnector | |||
subclass that handles opening the USB connection to a device. | |||
By default it will connect to the first Android or iOS device that | |||
has the Cozmo app running in SDK mode. | |||
force_on_top (bool): Specifies whether the window should be forced on top of all others | |||
''' | |||
try: | |||
from . import tkview | |||
except ImportError as exc: | |||
tkview = exc | |||
if isinstance(tkview, Exception): | |||
raise NotImplementedError('tkviewer not available on this platform; ' | |||
'make sure Tkinter, NumPy and Pillow packages are installed (%s)' % tkview) | |||
viewer = tkview.TkImageViewer(force_on_top=force_on_top) | |||
_connect_viewer(f, conn_factory, connector, viewer) | |||
def setup_basic_logging(general_log_level=None, protocol_log_level=None, | |||
protocol_log_messages=clad_protocol.LOG_ALL, target=sys.stderr, | |||
deprecated_filter="default"): | |||
'''Helper to perform basic setup of the Python logging machinery. | |||
The SDK defines two loggers: | |||
* :data:`logger` ("cozmo.general") - For general purpose information | |||
about events within the SDK; and | |||
* :data:`logger_protocol` ("cozmo.protocol") - For low level | |||
communication messages between the device and the SDK. | |||
Generally only :data:`logger` is interesting. | |||
Args: | |||
general_log_level (str): 'DEBUG', 'INFO', 'WARN', 'ERROR' or an equivalent | |||
constant from the :mod:`logging` module. If None then a | |||
value will be read from the COZMO_LOG_LEVEL environment variable. | |||
protocol_log_level (str): as general_log_level. If None then a | |||
value will be read from the COZMO_PROTOCOL_LOG_LEVEL environment | |||
variable. | |||
protocol_log_messages (list): The low level messages that should be | |||
logged to the protocol log. Defaults to all. Will read from | |||
the COMZO_PROTOCOL_LOG_MESSAGES if available which should be | |||
a comma separated list of message names (case sensitive). | |||
target (object): The stream to send the log data to; defaults to stderr | |||
deprecated_filter (str): The filter for any DeprecationWarning messages. | |||
This is defaulted to "default" which shows the warning once per | |||
location. You can hide all deprecated warnings by passing in "ignore", | |||
see https://docs.python.org/3/library/warnings.html#warning-filter | |||
for more information. | |||
''' | |||
if deprecated_filter is not None: | |||
warnings.filterwarnings(deprecated_filter, category=DeprecationWarning) | |||
if general_log_level is None: | |||
general_log_level = os.environ.get('COZMO_LOG_LEVEL', logging.INFO) | |||
if protocol_log_level is None: | |||
protocol_log_level = os.environ.get('COZMO_PROTOCOL_LOG_LEVEL', logging.INFO) | |||
if protocol_log_level: | |||
if 'COMZO_PROTOCOL_LOG_MESSAGES' in os.environ: | |||
lm = os.environ['COMZO_PROTOCOL_LOG_MESSAGES'] | |||
if lm.lower() == 'all': | |||
clad_protocol.CLADProtocol._clad_log_which = clad_protocol.LOG_ALL | |||
else: | |||
clad_protocol.CLADProtocol._clad_log_which = set(lm.split(',')) | |||
else: | |||
clad_protocol.CLADProtocol._clad_log_which = protocol_log_messages | |||
h = logging.StreamHandler(stream=target) | |||
f = logging.Formatter('%(asctime)s %(name)-12s %(levelname)-8s %(message)s') | |||
h.setFormatter(f) | |||
logger.addHandler(h) | |||
logger. setLevel(general_log_level) | |||
if protocol_log_level is not None: | |||
logger_protocol.addHandler(h) | |||
logger_protocol.setLevel(protocol_log_level) | |||
def run_program(f, use_viewer=False, conn_factory=conn.CozmoConnection, | |||
connector=None, force_viewer_on_top=False, | |||
deprecated_filter="default", use_3d_viewer=False, | |||
show_viewer_controls=True, | |||
exit_on_connection_error=True): | |||
'''Connect to Cozmo and run the provided program/function f. | |||
Args: | |||
f (callable): The function to execute, accepts a connected | |||
:class:`cozmo.robot.Robot` as the parameter. | |||
use_viewer (bool): Specifies whether to display a view of Cozmo's camera | |||
in a window. | |||
conn_factory (callable): Override the factory function to generate a | |||
:class:`cozmo.conn.CozmoConnection` (or subclass) instance. | |||
connector (:class:`DeviceConnector`): Optional instance of a DeviceConnector | |||
subclass that handles opening the USB connection to a device. | |||
By default it will connect to the first Android or iOS device that | |||
has the Cozmo app running in SDK mode. | |||
force_viewer_on_top (bool): Specifies whether the window should be | |||
forced on top of all others (only relevant if use_viewer is True). | |||
Note that this is ignored if use_3d_viewer is True (as it's not | |||
currently supported on that windowing system). | |||
deprecated_filter (str): The filter for any DeprecationWarning messages. | |||
This is defaulted to "default" which shows the warning once per | |||
location. You can hide all deprecated warnings by passing in "ignore", | |||
see https://docs.python.org/3/library/warnings.html#warning-filter | |||
for more information. | |||
use_3d_viewer (bool): Specifies whether to display a 3D view of Cozmo's | |||
understanding of the world in a window. Note that if both this and | |||
`use_viewer` are set then the 2D camera view will render in an OpenGL | |||
window instead of a TkView window. | |||
show_viewer_controls (bool): Specifies whether to draw controls on the view. | |||
exit_on_connection_error (bool): Specify whether the program should exit on | |||
connection error or should an error be raised. Default to true. | |||
''' | |||
setup_basic_logging(deprecated_filter=deprecated_filter) | |||
# Wrap f (a function that takes in an already created robot) | |||
# with a function that accepts a cozmo.conn.CozmoConnection | |||
if asyncio.iscoroutinefunction(f): | |||
@functools.wraps(f) | |||
async def wrapper(sdk_conn): | |||
try: | |||
robot = await sdk_conn.wait_for_robot() | |||
await f(robot) | |||
except exceptions.SDKShutdown: | |||
pass | |||
except KeyboardInterrupt: | |||
logger.info('Exit requested by user') | |||
else: | |||
@functools.wraps(f) | |||
def wrapper(sdk_conn): | |||
try: | |||
robot = sdk_conn.wait_for_robot() | |||
f(robot) | |||
except exceptions.SDKShutdown: | |||
pass | |||
except KeyboardInterrupt: | |||
logger.info('Exit requested by user') | |||
try: | |||
if use_3d_viewer: | |||
connect_with_3dviewer(wrapper, conn_factory=conn_factory, connector=connector, | |||
enable_camera_view=use_viewer, show_viewer_controls=show_viewer_controls) | |||
elif use_viewer: | |||
connect_with_tkviewer(wrapper, conn_factory=conn_factory, connector=connector, | |||
force_on_top=force_viewer_on_top) | |||
else: | |||
connect(wrapper, conn_factory=conn_factory, connector=connector) | |||
except KeyboardInterrupt: | |||
logger.info('Exit requested by user') | |||
except exceptions.ConnectionError as e: | |||
if exit_on_connection_error: | |||
sys.exit("A connection error occurred: %s" % e) | |||
else: | |||
logger.error("A connection error occurred: %s" % e) | |||
raise |
@@ -0,0 +1,128 @@ | |||
# 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. | |||
''' | |||
Song related classes, functions, events and values. | |||
''' | |||
# __all__ should order by constants, event classes, other classes, functions. | |||
__all__ = ['NoteTypes', 'NoteDurations', 'SongNote'] | |||
import collections | |||
from . import logger | |||
from . import action | |||
from . import exceptions | |||
from . import event | |||
from ._clad import _clad_to_engine_iface, _clad_to_engine_cozmo, _clad_to_engine_anki, CladEnumWrapper | |||
# generate names for each CLAD defined Note Type | |||
class _NoteType(collections.namedtuple('_NoteType', 'name id')): | |||
# Tuple mapping between CLAD SongNoteType. name and ID | |||
# All instances will be members of NoteTypes | |||
# Keep _NoteType as lightweight as a normal namedtuple | |||
__slots__ = () | |||
def __str__(self): | |||
return 'NoteTypes.%s' % self.name | |||
class NoteTypes(CladEnumWrapper): | |||
"""The possible values for an NoteType. | |||
A pitch between C2 and C3_Sharp can be specified, | |||
as well as a rest (for timed silence), giving | |||
cozmo a vocal range of slightly more than one | |||
octave. | |||
B_Flat and E_Flat are represented as their corresponding | |||
sharps. | |||
""" | |||
_clad_enum = _clad_to_engine_iface.SongNoteType | |||
_entry_type = _NoteType | |||
#: | |||
C2 = _entry_type("C2", _clad_enum.C2) | |||
#: | |||
C2_Sharp = _entry_type("C2_Sharp", _clad_enum.C2_Sharp) | |||
#: | |||
D2 = _entry_type("D2", _clad_enum.D2) | |||
#: | |||
D2_Sharp = _entry_type("D2_Sharp", _clad_enum.D2_Sharp) | |||
#: | |||
E2 = _entry_type("E2", _clad_enum.E2) | |||
#: | |||
F2 = _entry_type("F2", _clad_enum.F2) | |||
#: | |||
F2_Sharp = _entry_type("F2_Sharp", _clad_enum.F2_Sharp) | |||
#: | |||
G2 = _entry_type("G2", _clad_enum.G2) | |||
#: | |||
G2_Sharp = _entry_type("G2_Sharp", _clad_enum.G2_Sharp) | |||
#: | |||
A2 = _entry_type("A2", _clad_enum.A2) | |||
#: | |||
A2_Sharp = _entry_type("A2_Sharp", _clad_enum.A2_Sharp) | |||
#: | |||
B2 = _entry_type("B2", _clad_enum.B2) | |||
#: | |||
C3 = _entry_type("C3", _clad_enum.C3) | |||
#: | |||
C3_Sharp = _entry_type("C3_Sharp", _clad_enum.C3_Sharp) | |||
#: | |||
Rest = _entry_type("Rest", _clad_enum.Rest) | |||
NoteTypes._init_class(warn_on_missing_definitions=True) | |||
# generate names for each CLAD defined Note Duration | |||
class _NoteDuration(collections.namedtuple('_NoteDuration', 'name id')): | |||
# Tuple mapping between CLAD SongNoteDuration. name and ID | |||
# All instances will be members of NoteTypes | |||
# Keep _NoteDuration as lightweight as a normal namedtuple | |||
__slots__ = () | |||
def __str__(self): | |||
return 'NoteDurations.%s' % self.name | |||
class NoteDurations(CladEnumWrapper): | |||
"""The possible values for a NoteDuration. | |||
""" | |||
_clad_enum = _clad_to_engine_iface.SongNoteDuration | |||
_entry_type = _NoteDuration | |||
#: | |||
Whole = _entry_type("Whole", _clad_enum.Whole) | |||
#: | |||
ThreeQuarter = _entry_type("ThreeQuarter", _clad_enum.ThreeQuarter) | |||
#: | |||
Half = _entry_type("Half", _clad_enum.Half) | |||
#: | |||
Quarter = _entry_type("Quarter", _clad_enum.Quarter) | |||
NoteDurations._init_class(warn_on_missing_definitions=True) | |||
class SongNote(_clad_to_engine_iface.SongNote): | |||
"""Represents on element in a song. Consists of a :class:`cozmo.song.NoteTypes` which specifies | |||
either a pitch or rest, and a :class:`cozmo.song.NoteDurations` specifying the length of the | |||
note. | |||
""" | |||
def __init__(self, noteType=NoteTypes.C2, noteDuration=NoteDurations.Whole): | |||
super(SongNote, self).__init__(noteType.id, noteDuration.id) |
@@ -0,0 +1,165 @@ | |||
# 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. | |||
'''This module provides a simple GUI viewer for Cozmo's camera. | |||
It uses Tkinter, the standard Python GUI toolkit which is optionally available | |||
on most platforms, and also depends on the Pillow and numpy libraries for | |||
image processing. | |||
See the online SDK documentation for details on how to install these extra | |||
packages on your platform. | |||
The easiest way to make use of this viewer is to call | |||
:func:`cozmo.run.connect_with_tkviewer`. | |||
Warning: | |||
This package requires Python to have Tkinter installed to display the GUI. | |||
''' | |||
# __all__ should order by constants, event classes, other classes, functions. | |||
__all__ = ['TkImageViewer'] | |||
import cozmo | |||
import collections | |||
import functools | |||
import queue | |||
import platform | |||
import time | |||
from PIL import Image, ImageDraw, ImageTk | |||
import tkinter | |||
from . import world | |||
class TkThreadable: | |||
'''A mixin for adding threadsafe calls to tkinter methods.''' | |||
#pylint: disable=no-member | |||
# no-member errors are raised in pylint regarding members/methods called but not defined in our mixin. | |||
def __init__(self, *a, **kw): | |||
self._thread_queue = queue.Queue() | |||
self.after(50, self._thread_call_dispatch) | |||
def call_threadsafe(self, method, *a, **kw): | |||
self._thread_queue.put((method, a, kw)) | |||
def _thread_call_dispatch(self): | |||
while True: | |||
try: | |||
method, a, kw = self._thread_queue.get(block=False) | |||
self.after_idle(method, *a, **kw) | |||
except queue.Empty: | |||
break | |||
self.after(50, self._thread_call_dispatch) | |||
class TkImageViewer(tkinter.Frame, TkThreadable): | |||
'''Simple Tkinter camera viewer.''' | |||
# TODO: rewrite this whole thing. Make a generic camera widget | |||
# that can be used in other Tk applications. Also handle resizing | |||
# the window properly. | |||
def __init__(self, | |||
tk_root=None, refresh_interval=10, image_scale = 2, | |||
window_name = "CozmoView", force_on_top=True): | |||
if tk_root is None: | |||
tk_root = tkinter.Tk() | |||
tkinter.Frame.__init__(self, tk_root) | |||
TkThreadable.__init__(self) | |||
self._img_queue = collections.deque(maxlen=1) | |||
self._refresh_interval = refresh_interval | |||
self.scale = image_scale | |||
self.width = None | |||
self.height = None | |||
self.tk_root = tk_root | |||
tk_root.wm_title(window_name) | |||
# Tell the TK root not to resize based on the contents of the window. | |||
# Necessary to get the resizing to function properly | |||
tk_root.pack_propagate(False) | |||
# Set the starting window size | |||
tk_root.geometry('{}x{}'.format(720, 540)) | |||
self.label = tkinter.Label(self.tk_root,image=None) | |||
self.tk_root.protocol("WM_DELETE_WINDOW", self._delete_window) | |||
self._isRunning = True | |||
self.robot = None | |||
self.handler = None | |||
self._first_image = True | |||
tk_root.aspect(4,3,4,3) | |||
if force_on_top: | |||
# force window on top of all others, regardless of focus | |||
tk_root.wm_attributes("-topmost", 1) | |||
self.tk_root.bind("<Configure>", self.configure) | |||
self._repeat_draw_frame() | |||
async def connect(self, coz_conn): | |||
self.robot = await coz_conn.wait_for_robot() | |||
self.robot.camera.image_stream_enabled = True | |||
self.handler = self.robot.world.add_event_handler( | |||
world.EvtNewCameraImage, self.image_event) | |||
def disconnect(self): | |||
if self.handler: | |||
self.handler.disable() | |||
self.call_threadsafe(self.quit) | |||
# The base class configure doesn't take an event | |||
#pylint: disable=arguments-differ | |||
def configure(self, event): | |||
if event.width < 50 or event.height < 50: | |||
return | |||
self.height = event.height | |||
self.width = event.width | |||
def image_event(self, evt, *, image, **kw): | |||
if self._first_image or self.width is None: | |||
img = image.annotate_image(scale=self.scale) | |||
else: | |||
img = image.annotate_image(fit_size=(self.width, self.height)) | |||
self._img_queue.append(img) | |||
def _delete_window(self): | |||
self.tk_root.destroy() | |||
self.quit() | |||
self._isRunning = False | |||
def _draw_frame(self): | |||
if ImageTk is None: | |||
return | |||
try: | |||
image = self._img_queue.popleft() | |||
except IndexError: | |||
# no new image | |||
return | |||
self._first_image = False | |||
photoImage = ImageTk.PhotoImage(image) | |||
self.label.configure(image=photoImage) | |||
self.label.image = photoImage | |||
# Dynamically expand the image to fit the window. And fill in both X and Y directions. | |||
self.label.pack(fill=tkinter.BOTH, expand=True) | |||
def _repeat_draw_frame(self, event=None): | |||
self._draw_frame() | |||
self.after(self._refresh_interval, self._repeat_draw_frame) |
@@ -0,0 +1,15 @@ | |||
# Copyright (c) 2016 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. | |||
from .usbmux import * |
@@ -0,0 +1,470 @@ | |||
# Copyright (c) 2016 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. | |||
__all__ = ('USBMuxError', 'ProtocolError', 'DeviceNotConnected', | |||
'ConnectionRefused', 'ConnectionFailed', 'QueueNotifyCM', 'USBMux', | |||
'connect_to_usbmux') | |||
import asyncio | |||
import collections | |||
import contextlib | |||
import plistlib | |||
import socket | |||
import struct | |||
import sys | |||
import time | |||
DEFAULT_SOCKET_PATH = '/var/run/usbmuxd' | |||
DEFAULT_SOCKET_PORT = 27015 | |||
DEFAULT_MAX_WAIT = 2 | |||
PLIST_VERSION = 1 | |||
ACTION_ATTACHED = 'attached' | |||
ACTION_DETACHED = 'detached' | |||
class USBMuxError(Exception): pass | |||
class ProtocolError(USBMuxError): pass | |||
class DeviceNotConnected(USBMuxError): pass | |||
class ConnectionRefused(USBMuxError): pass | |||
class ConnectionFailed(USBMuxError): pass | |||
class PlistProto(asyncio.Protocol): | |||
def connection_made(self, transport): | |||
self.transport = transport | |||
self._buf = bytearray() | |||
def data_received(self, data): | |||
self._buf += data | |||
while len(self._buf) > 4: | |||
length = struct.unpack('I', self._buf[:4])[0] | |||
if len(self._buf) < length: | |||
return | |||
ver, req, tag = struct.unpack('III', self._buf[4:16]) | |||
if ver != PLIST_VERSION: | |||
raise ProtocolError("Unsupported protocol version from usbmux stream") | |||
pldata = plistlib.loads(self._buf[16:length]) | |||
self.msg_received(pldata) | |||
self._buf = self._buf[length:] | |||
def send_msg(self, **kw): | |||
pl = plistlib.dumps(kw) | |||
self.transport.write(struct.pack('IIII', len(pl) + 16, 1, 8, 1)) | |||
self.transport.write(pl) | |||
def msg_received(self, msg): | |||
'''Called when a plist record is received''' | |||
class USBMuxConnector(PlistProto): | |||
'''Opens a connection to a port on a device''' | |||
def __init__(self, device_id, port, waiter): | |||
self.device_id = device_id | |||
self.port = port | |||
self.waiter = waiter | |||
def connection_made(self, transport): | |||
super().connection_made(transport) | |||
self.send_msg( | |||
MessageType='Connect', | |||
ClientVersionString='pyusbmux', | |||
ProgName='pyusbmux', | |||
DeviceID=self.device_id, | |||
PortNumber=socket.htons(self.port) | |||
) | |||
def connection_lost(self, exc): | |||
if self.waiter.done(): | |||
return | |||
self.waiter.set_exception(exc) | |||
def msg_received(self, msg): | |||
if msg['MessageType'] != 'Result': | |||
return | |||
status = msg['Number'] | |||
if status == 0: | |||
self.waiter.set_result(None) | |||
# ensure no futher data is received until the protocol | |||
# is switched to the application protocol. | |||
self.transport.pause_reading() | |||
elif status == 2: | |||
self.waiter.set_exception(DeviceNotConnected("Device %s is not currently connected" % (self.device_id,))) | |||
elif status == 3: | |||
self.waiter.set_exception(ConnectionRefused("Connection refused to device_id=%s port=%d" % (self.device_id, self.port))) | |||
else: | |||
self.waiter.set_exception(ConnectionFailed("Protocol error connecting to device %s" % (self.device_id,))) | |||
class _ProtoSwitcher(asyncio.Protocol): | |||
def __init__(self, loop, initial_protocol): | |||
self._loop = loop | |||
self._transport = None | |||
self.protocol = initial_protocol | |||
def switch_protocol(self, protocol_factory): | |||
self.protocol = protocol_factory() | |||
if self._transport: | |||
self._loop.call_soon(self.protocol.connection_made, self._transport) | |||
self._loop.call_soon(self._transport.resume_reading) | |||
return self.protocol | |||
def connection_made(self, transport): | |||
self._transport = transport | |||
self.protocol.connection_made(transport) | |||
def connection_lost(self, exc): | |||
self.protocol.connection_lost(exc) | |||
def pause_writing(self): | |||
self.protocol.pause_writing() | |||
def resume_writing(self): | |||
self.protocol.resume_writing() | |||
def data_received(self, data): | |||
self.protocol.data_received(data) | |||
def eof_received(self): | |||
self.protocol.eof_received() | |||
class USBMux(PlistProto): | |||
'''USBMux wraps a connection to the USBMux daemon. | |||
Use ``connect_to_usbmux`` or call ``connect`` on an instance of this | |||
class to connect to the daemon. | |||
Once connected, the ``attached`` attribute is populated with a dictionary | |||
keyed by an integer device id, and with a dictionary of values about the | |||
connected device. | |||
The ``attached`` dictionary is populated asynchronously and may be empty | |||
after ``connect`` returns. | |||
Subclasses of USBMux will have their ``device_attached`` and | |||
``device_detached`` methods called as devices are made available through | |||
the connected mux. | |||
Alternatively call :meth:`wait_for_attach` to wait for a new device to | |||
be made available, or use the :meth:`attach_watcher` method to obtain | |||
a context manager to iterate over all devices as they connect and | |||
disconect. | |||
The ``connect`` call will open a TCP connection to a specific port | |||
on a specific device. :meth:`connect_to_first` can be used if it doesn't | |||
matter which device is connected to, as long as the requested port is open. | |||
''' | |||
def __init__(self, loop, mux_socket_path=DEFAULT_SOCKET_PATH, mux_socket_port=DEFAULT_SOCKET_PORT): | |||
#: Currently attached devices, keyed by integer device_id | |||
self.attached = {} | |||
self.loop = loop | |||
self.mux_socket_path = mux_socket_path | |||
self.mux_socket_port = mux_socket_port | |||
self._attach_notify = QueueNotify(loop=loop) | |||
async def _connect_transport(self, protocol_factory): | |||
if sys.platform in ('win32', 'cygwin'): | |||
return await self.loop.create_connection(protocol_factory, host='127.0.0.1', port=self.mux_socket_port) | |||
else: | |||
result = await self.loop.create_unix_connection(protocol_factory, self.mux_socket_path) | |||
return result | |||
async def connect(self): | |||
'''Opens a connection to the USBMux daemon on the local machine. | |||
:func:`connect_to_usbmux` provides a convenient wrapper to this method. | |||
''' | |||
self._waiter = asyncio.Future(loop=self.loop) | |||
await self._connect_transport(lambda: self) | |||
await self._waiter | |||
def connection_made(self, transport): | |||
super().connection_made(transport) | |||
self.send_msg( | |||
MessageType='Listen', | |||
ClientVersionString='pyusbmux', | |||
ProgName='pyusbmux' | |||
) | |||
def connection_lost(self, exc): | |||
super().connection_lost(exc) | |||
if not self._waiter.done(): | |||
self._waiter.set_exception(exc) | |||
def msg_received(self, msg): | |||
mt = msg.get('MessageType') | |||
if mt == 'Result': | |||
if msg['Number'] == 0: | |||
self._waiter.set_result(None) | |||
else: | |||
self._waiter.set_exception(ConnectionFailed()) | |||
elif mt == 'Attached': | |||
device_id = msg['Properties']['DeviceID'] | |||
self.attached[device_id] = msg['Properties'] | |||
self.device_attached(device_id, msg['Properties']) | |||
self._attach_notify.notify((ACTION_ATTACHED, device_id, msg['Properties'])) | |||
elif mt == 'Detached': | |||
device_id = msg['DeviceID'] | |||
if device_id in self.attached: | |||
props = self.attached[device_id] | |||
del(self.attached[device_id]) | |||
self._attach_notify.notify((ACTION_DETACHED, device_id, props)) | |||
self.device_detached(device_id) | |||
async def connect_to_device(self, protocol_factory, device_id, port): | |||
'''Open a TCP connection to a port on a device. | |||
Args: | |||
protocol_factory (callable): A callable that returns an asyncio.Protocol implementation | |||
device_id (int): The id of the device to connect to | |||
port (int): The port to connect to on the target device | |||
Returns: | |||
(dict, asyncio.Transport, asyncio.Protocol): The device information, | |||
connected transport and protocol. | |||
Raises: | |||
A USBMuxError subclass instance such as ConnectionRefused | |||
''' | |||
waiter = asyncio.Future(loop=self.loop) | |||
connector = USBMuxConnector(device_id, port, waiter) | |||
transport, switcher = await self._connect_transport(lambda: _ProtoSwitcher(self.loop, connector)) | |||
# wait for the connection to succeed or fail | |||
await waiter | |||
app_protocol = switcher.switch_protocol(protocol_factory) | |||
device_info = self.attached.get(device_id) or {} | |||
return device_info, transport, app_protocol | |||
async def wait_for_serial(self, serial, timeout=DEFAULT_MAX_WAIT): | |||
'''Wait for a device with the specified serial number to attach. | |||
Args: | |||
serial (string): Serial number of the device to wait for. | |||
timeout (float): The maximum amount of time in seconds to wait for a | |||
matching device to be connected. | |||
Set to None to wait indefinitely, or -1 to only check currently | |||
connected devices. | |||
Returns: | |||
int: The device id of the connected device | |||
Raises: | |||
asyncio.TimeoutError if the device with the specified serial number doesn't appear. | |||
''' | |||
timeout = Timeout(timeout) | |||
with self.attach_watcher(include_existing=True) as watcher: | |||
while not timeout.expired: | |||
action, device_id, info = await watcher.wait_for_next(timeout.remaining) | |||
if action != ACTION_ATTACHED: | |||
continue | |||
if info['SerialNumber'].lower() == serial.lower(): | |||
return device_id | |||
raise asyncio.TimeoutError("No devices matching serial number found") | |||
async def connect_to_first_device(self, protocol_factory, port, timeout=DEFAULT_MAX_WAIT, | |||
include=None, exclude=None): | |||
'''Open a TCP connection to the first device that has the requested port open. | |||
Args: | |||
protocol_factory (callable): A callable that returns an asyncio.Protocol implementation. | |||
port (int): The port to connect to on the target device. | |||
timeout (float): The maximum amount of time to wait for a suitable device to be connected. | |||
Returns: | |||
(dict, asyncio.Transport, asyncio.Protocol): The device information, | |||
connected transport and protocol. | |||
Raises: | |||
asyncio.TimeoutError if no devices with the requested port become | |||
available in the specified time. | |||
''' | |||
with self.attach_watcher(include_existing=True) as watcher: | |||
timeout = Timeout(timeout) | |||
while not timeout.expired: | |||
action, device_id, info = await watcher.wait_for_next(timeout.remaining) | |||
if action != ACTION_ATTACHED: | |||
continue | |||
if exclude is not None and device_id in exclude: | |||
continue | |||
if include is not None and device_id not in include: | |||
continue | |||
try: | |||
return await self.connect_to_device(protocol_factory, device_id, port) | |||
except USBMuxError: | |||
pass | |||
raise asyncio.TimeoutError("No available devices") | |||
async def wait_for_attach(self, timeout=None): | |||
'''Wait for the next device attachment event. | |||
Args: | |||
timeout (float): Maximum amount of time to wait for an event, or None for no timeout | |||
Returns: | |||
int: The device id that attached. | |||
Raises: | |||
asyncio.TimeoutError if no devices with the requested port become | |||
available in the specified time. | |||
''' | |||
timeout = Timeout(timeout) | |||
with self.attach_watcher() as watcher: | |||
while True: | |||
action, device_id, info = await watcher.wait_for_next(timeout.remaining) | |||
if action == ACTION_ATTACHED: | |||
return device_id | |||
def attach_watcher(self, include_existing=False): | |||
'''Returns a context manager that will record and make available all attach/detach notifications. | |||
The context manager yields events consisting of (action, device_id, device_info) tuples, | |||
where ``action`` is either :const:`ACTION_ATTACHED` or :const:`ACTION_DETACHED` | |||
and ``device_info`` is a dictionary of information specific to that device, | |||
such as the serial number. | |||
Args: | |||
include_existing (bool): If True then a stream of fake attached events | |||
will be generated for all existing connected devices ahead of | |||
monitoring for newly attached devices. | |||
Returns: | |||
:class:`QueueNotifyCM` | |||
''' | |||
initial_data = None | |||
if include_existing: | |||
initial_data = [(ACTION_ATTACHED, device_id, info) | |||
for (device_id, info) in self.attached.items()] | |||
return self._attach_notify.get_contextmanager(initial_data=initial_data) | |||
def device_attached(self, device_id, properties): | |||
pass | |||
def device_detached(self, device_id): | |||
pass | |||
async def connect_to_usbmux(mux_socket_path=DEFAULT_SOCKET_PATH, mux_socket_port=DEFAULT_SOCKET_PORT, loop=None): | |||
'''Connect to a USBMux endpoint. | |||
Args: | |||
mux_socket_path (string) - The path of the Unix socket of the mux daemon (used on non Windows platforms) | |||
mux_socket_port (int) - The TCP port number of the mux daemon (used on Windows platforms) | |||
loop (asyncio.BaseLoop) - Event loop to connect on; defaults to the current active event loop | |||
Returns: | |||
USBMux instance | |||
Raises: | |||
Exception on connection refused or other error | |||
''' | |||
if loop is None: | |||
loop = asyncio.get_event_loop() | |||
mux = USBMux(loop, mux_socket_path=mux_socket_path, mux_socket_port=mux_socket_port) | |||
await mux.connect() | |||
return mux | |||
class QueueNotify: | |||
'''Provide a context manager to queue and read asynchronous notifications. | |||
While the context manager is active, all notifications are queued and | |||
read by calling ``wait_for_next`` on the returned QueueNotifyCM | |||
object. If none are available, then the method will wait for the specified | |||
amount of time for a new entry to arrive. | |||
Multiple context managers can be active concurrently receiving the same | |||
notifications. | |||
''' | |||
def __init__(self, loop=None): | |||
self.loop = loop | |||
self._active = set() | |||
def notify(self, value): | |||
for entry in self._active: | |||
entry._notify(value) | |||
def get_contextmanager(self, initial_data=None, max_qsize=None): | |||
ctx = QueueNotifyCM(self, initial_data=initial_data, max_qsize=max_qsize, loop=self.loop) | |||
self._active.add(ctx) | |||
return ctx | |||
def context_done(self, ctx): | |||
self._active.discard(ctx) | |||
class QueueNotifyCM: | |||
'''Helper class for QueueNotify.''' | |||
def __init__(self, mgr, initial_data=None, max_qsize=None, loop=None): | |||
self.loop = loop | |||
self._mgr = mgr | |||
self._wake = None | |||
if initial_data is None: | |||
initial_data = [] | |||
self._q = collections.deque(initial_data, max_qsize) | |||
def __enter__(self): | |||
return self | |||
def __exit__(self, exc_type, exc_val, exc_tb): | |||
self._mgr.context_done(self) | |||
return False | |||
def _notify(self, item): | |||
self._q.append(item) | |||
if self._wake is not None and not self._wake.done(): | |||
self._wake.set_result(True) | |||
self._wake = None | |||
async def wait_for_next(self, timeout=None): | |||
'''Wait for the next available notification. | |||
Will return immediately if entries are already waiting to be read, | |||
else wait up to ``timeout`` seconds for a new entry to arrive. | |||
''' | |||
try: | |||
return self._q.popleft() | |||
except IndexError: | |||
pass | |||
self._wake = asyncio.Future(loop=self.loop) | |||
await asyncio.wait_for(self._wake, loop=self.loop, timeout=timeout) | |||
return self._q.popleft() | |||
class Timeout: | |||
'''Helper class to track timeout state.''' | |||
def __init__(self, timeout=None): | |||
self.timeout = timeout | |||
self.start = time.time() | |||
@property | |||
def remaining(self): | |||
if self.timeout is None: | |||
return None | |||
return self.timeout - (time.time() - self.start) | |||
@property | |||
def expired(self): | |||
return self.timeout is not None and self.remaining <= 0 |
@@ -0,0 +1,24 @@ | |||
# 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. | |||
__version__ = "1.4.10" | |||
# Specify the version of cozmoclad that this package requires | |||
# Releases of the Cozmo package must specify an exact cozmoclad release | |||
# to ensure compatibility with a specific release of the ios/android app. | |||
__cozmoclad_version__ = "3.4.0" | |||
#__cozmoclad_version__ = "1.7.1" | |||
# Minimum cozmoclad version supported by the API | |||
__min_cozmoclad_version__ = "2.0.0" |
@@ -0,0 +1 @@ | |||
pip |
@@ -0,0 +1,14 @@ | |||
// 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. | |||
@@ -0,0 +1,14 @@ | |||
# 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. | |||
@@ -0,0 +1,180 @@ | |||
Unless otherwise stated in that file, or the folder containing that file, all | |||
files in the Cozmo SDK are Copyright (c) 2016-2017 Anki Inc. and licensed under | |||
the Apache 2.0 License: | |||
Apache License | |||
Version 2.0, January 2004 | |||
http://www.apache.org/licenses/ | |||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION | |||
1. Definitions. | |||
"License" shall mean the terms and conditions for use, reproduction, | |||
and distribution as defined by Sections 1 through 9 of this document. | |||
"Licensor" shall mean the copyright owner or entity authorized by | |||
the copyright owner that is granting the License. | |||
"Legal Entity" shall mean the union of the acting entity and all | |||
other entities that control, are controlled by, or are under common | |||
control with that entity. For the purposes of this definition, | |||
"control" means (i) the power, direct or indirect, to cause the | |||
direction or management of such entity, whether by contract or | |||
otherwise, or (ii) ownership of fifty percent (50%) or more of the | |||
outstanding shares, or (iii) beneficial ownership of such entity. | |||
"You" (or "Your") shall mean an individual or Legal Entity | |||
exercising permissions granted by this License. | |||
"Source" form shall mean the preferred form for making modifications, | |||
including but not limited to software source code, documentation | |||
source, and configuration files. | |||
"Object" form shall mean any form resulting from mechanical | |||
transformation or translation of a Source form, including but | |||
not limited to compiled object code, generated documentation, | |||
and conversions to other media types. | |||
"Work" shall mean the work of authorship, whether in Source or | |||
Object form, made available under the License, as indicated by a | |||
copyright notice that is included in or attached to the work | |||
(an example is provided in the Appendix below). | |||
"Derivative Works" shall mean any work, whether in Source or Object | |||
form, that is based on (or derived from) the Work and for which the | |||
editorial revisions, annotations, elaborations, or other modifications | |||
represent, as a whole, an original work of authorship. For the purposes | |||
of this License, Derivative Works shall not include works that remain | |||
separable from, or merely link (or bind by name) to the interfaces of, | |||
the Work and Derivative Works thereof. | |||
"Contribution" shall mean any work of authorship, including | |||
the original version of the Work and any modifications or additions | |||
to that Work or Derivative Works thereof, that is intentionally | |||
submitted to Licensor for inclusion in the Work by the copyright owner | |||
or by an individual or Legal Entity authorized to submit on behalf of | |||
the copyright owner. For the purposes of this definition, "submitted" | |||
means any form of electronic, verbal, or written communication sent | |||
to the Licensor or its representatives, including but not limited to | |||
communication on electronic mailing lists, source code control systems, | |||
and issue tracking systems that are managed by, or on behalf of, the | |||
Licensor for the purpose of discussing and improving the Work, but | |||
excluding communication that is conspicuously marked or otherwise | |||
designated in writing by the copyright owner as "Not a Contribution." | |||
"Contributor" shall mean Licensor and any individual or Legal Entity | |||
on behalf of whom a Contribution has been received by Licensor and | |||
subsequently incorporated within the Work. | |||
2. Grant of Copyright License. Subject to the terms and conditions of | |||
this License, each Contributor hereby grants to You a perpetual, | |||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable | |||
copyright license to reproduce, prepare Derivative Works of, | |||
publicly display, publicly perform, sublicense, and distribute the | |||
Work and such Derivative Works in Source or Object form. | |||
3. Grant of Patent License. Subject to the terms and conditions of | |||
this License, each Contributor hereby grants to You a perpetual, | |||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable | |||
(except as stated in this section) patent license to make, have made, | |||
use, offer to sell, sell, import, and otherwise transfer the Work, | |||
where such license applies only to those patent claims licensable | |||
by such Contributor that are necessarily infringed by their | |||
Contribution(s) alone or by combination of their Contribution(s) | |||
with the Work to which such Contribution(s) was submitted. If You | |||
institute patent litigation against any entity (including a | |||
cross-claim or counterclaim in a lawsuit) alleging that the Work | |||
or a Contribution incorporated within the Work constitutes direct | |||
or contributory patent infringement, then any patent licenses | |||
granted to You under this License for that Work shall terminate | |||
as of the date such litigation is filed. | |||
4. Redistribution. You may reproduce and distribute copies of the | |||
Work or Derivative Works thereof in any medium, with or without | |||
modifications, and in Source or Object form, provided that You | |||
meet the following conditions: | |||
(a) You must give any other recipients of the Work or | |||
Derivative Works a copy of this License; and | |||
(b) You must cause any modified files to carry prominent notices | |||
stating that You changed the files; and | |||
(c) You must retain, in the Source form of any Derivative Works | |||
that You distribute, all copyright, patent, trademark, and | |||
attribution notices from the Source form of the Work, | |||
excluding those notices that do not pertain to any part of | |||
the Derivative Works; and | |||
(d) If the Work includes a "NOTICE" text file as part of its | |||
distribution, then any Derivative Works that You distribute must | |||
include a readable copy of the attribution notices contained | |||
within such NOTICE file, excluding those notices that do not | |||
pertain to any part of the Derivative Works, in at least one | |||
of the following places: within a NOTICE text file distributed | |||
as part of the Derivative Works; within the Source form or | |||
documentation, if provided along with the Derivative Works; or, | |||
within a display generated by the Derivative Works, if and | |||
wherever such third-party notices normally appear. The contents | |||
of the NOTICE file are for informational purposes only and | |||
do not modify the License. You may add Your own attribution | |||
notices within Derivative Works that You distribute, alongside | |||
or as an addendum to the NOTICE text from the Work, provided | |||
that such additional attribution notices cannot be construed | |||
as modifying the License. | |||
You may add Your own copyright statement to Your modifications and | |||
may provide additional or different license terms and conditions | |||
for use, reproduction, or distribution of Your modifications, or | |||
for any such Derivative Works as a whole, provided Your use, | |||
reproduction, and distribution of the Work otherwise complies with | |||
the conditions stated in this License. | |||
5. Submission of Contributions. Unless You explicitly state otherwise, | |||
any Contribution intentionally submitted for inclusion in the Work | |||
by You to the Licensor shall be under the terms and conditions of | |||
this License, without any additional terms or conditions. | |||
Notwithstanding the above, nothing herein shall supersede or modify | |||
the terms of any separate license agreement you may have executed | |||
with Licensor regarding such Contributions. | |||
6. Trademarks. This License does not grant permission to use the trade | |||
names, trademarks, service marks, or product names of the Licensor, | |||
except as required for reasonable and customary use in describing the | |||
origin of the Work and reproducing the content of the NOTICE file. | |||
7. Disclaimer of Warranty. Unless required by applicable law or | |||
agreed to in writing, Licensor provides the Work (and each | |||
Contributor provides its Contributions) on an "AS IS" BASIS, | |||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | |||
implied, including, without limitation, any warranties or conditions | |||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A | |||
PARTICULAR PURPOSE. You are solely responsible for determining the | |||
appropriateness of using or redistributing the Work and assume any | |||
risks associated with Your exercise of permissions under this License. | |||
8. Limitation of Liability. In no event and under no legal theory, | |||
whether in tort (including negligence), contract, or otherwise, | |||
unless required by applicable law (such as deliberate and grossly | |||
negligent acts) or agreed to in writing, shall any Contributor be | |||
liable to You for damages, including any direct, indirect, special, | |||
incidental, or consequential damages of any character arising as a | |||
result of this License or out of the use or inability to use the | |||
Work (including but not limited to damages for loss of goodwill, | |||
work stoppage, computer failure or malfunction, or any and all | |||
other commercial damages or losses), even if such Contributor | |||
has been advised of the possibility of such damages. | |||
9. Accepting Warranty or Additional Liability. While redistributing | |||
the Work or Derivative Works thereof, You may choose to offer, | |||
and charge a fee for, acceptance of support, warranty, indemnity, | |||
or other liability obligations and/or rights consistent with this | |||
License. However, in accepting such obligations, You may act only | |||
on Your own behalf and on Your sole responsibility, not on behalf | |||
of any other Contributor, and only if You agree to indemnify, | |||
defend, and hold each Contributor harmless for any liability | |||
incurred by, or claims asserted against, such Contributor by reason | |||
of your accepting any such warranty or additional liability. | |||
END OF TERMS AND CONDITIONS |
@@ -0,0 +1,24 @@ | |||
Metadata-Version: 2.1 | |||
Name: cozmoclad | |||
Version: 3.4.0 | |||
Summary: Low-level protocol for the Anki Cozmo SDK. | |||
Home-page: https://developer.anki.com/cozmo/ | |||
Author: Anki, Inc | |||
Author-email: cozmosdk@anki.com | |||
License: Apache License, Version 2.0 | |||
Platform: UNKNOWN | |||
Classifier: Development Status :: 4 - Beta | |||
Classifier: Intended Audience :: Developers | |||
Classifier: Topic :: Software Development :: Libraries | |||
Classifier: License :: OSI Approved :: Apache Software License | |||
Classifier: Programming Language :: Python :: 3.5 | |||
Cozmo, by Anki. | |||
Cozmo is a small robot with a big personality. | |||
This library provides a low-level protocol library used by the | |||
cozmo SDK package. | |||
@@ -0,0 +1,218 @@ | |||
cozmoclad/LICENSE.txt,sha256=THPe12iw4yd8mbwjmhwsGiCsmC2QDopJnGNSbSh9hkc,10356 | |||
cozmoclad/__init__.py,sha256=3YzyCjyjq9dt4a_MKiWfDnzjmAwmtumQ0kb_84ieewo,2697 | |||
cozmoclad/clad/__init__.py,sha256=M-ndufPpUa5HCGUZpUi3XeMvb5T1rM3xZhf0stXJ0BQ,611 | |||
cozmoclad/clad/audio/__init__.py,sha256=M-ndufPpUa5HCGUZpUi3XeMvb5T1rM3xZhf0stXJ0BQ,611 | |||
cozmoclad/clad/audio/audioBusTypes.py,sha256=qqR1r4Amk0kOPPKP3GNARjbD73KvJZYbizizuI58NM4,2324 | |||
cozmoclad/clad/audio/audioCallbackMessage.py,sha256=zs8f1UwyU9fY1lOgtKv7sJvdL-sbKx8886UMeYUyPV0,25374 | |||
cozmoclad/clad/audio/audioEventTypes.py,sha256=ugLhi-ur71ifdRmq0GP2g4YfSGTtwHNWgSqj2Oq0WxY,117496 | |||
cozmoclad/clad/audio/audioGameObjectTypes.py,sha256=7E-CGxe4SrQ4NegnXy5yjwkapp8QrVjOOnBfHucCypY,2004 | |||
cozmoclad/clad/audio/audioMessage.py,sha256=VhfO8sEyUiY0_3eZOQWbSIo9pyOZ2RmRTqBRvsaUoOQ,24880 | |||
cozmoclad/clad/audio/audioMessageTypes.py,sha256=IbZ2X5DhxAjGM5U210qR5q6Hb13KUjWIV0OhLhRcLaI,2280 | |||
cozmoclad/clad/audio/audioParameterTypes.py,sha256=weZpAaYUBmFWg0cOGLMb8usvACyTcjKmTn1BYEAIHSU,3247 | |||
cozmoclad/clad/audio/audioSoundbanks.py,sha256=YWS1Ag13V7nPgrMGsKf5ra2v29cRwFwzAQc_tn8df80,1945 | |||
cozmoclad/clad/audio/audioStateTypes.py,sha256=87Bm6ONwzTFZKioXC2GFf6S4hTEzRKmGiLlFjWAvAKs,5444 | |||
cozmoclad/clad/audio/audioSwitchTypes.py,sha256=KUbgxX2KlPswnnMUlOwpEeOGxpnLZcN-XQbGPF-XBrU,15835 | |||
cozmoclad/clad/audio/messageAudioClient.py,sha256=LvG4xxfDux-vFk0ZuwSKRUh3tTRFRbEaWQsSIEO8T-w,12978 | |||
cozmoclad/clad/externalInterface/__init__.py,sha256=M-ndufPpUa5HCGUZpUi3XeMvb5T1rM3xZhf0stXJ0BQ,611 | |||
cozmoclad/clad/externalInterface/messageActions.py,sha256=rvq2kjFk-pZAU14Of4fGHoUkTBseVssgEwS9w24IeSI,284155 | |||
cozmoclad/clad/externalInterface/messageEngineToGame.py,sha256=OqDmbDl5Z9pr10vA2BMGi3lQTVwxsXELeTwrasICDQg,599991 | |||
cozmoclad/clad/externalInterface/messageEngineToGame_hash.py,sha256=XR3ekFiXy3-UHP7OQtiVjpnVvhCL-nbV_yFO8LJDUVE,1584 | |||
cozmoclad/clad/externalInterface/messageGameToEngine.py,sha256=x910Wpbzic4PxT0TLkAyo9k7748bHUHH92dsMB2V-BQ,788022 | |||
cozmoclad/clad/externalInterface/messageGameToEngine_hash.py,sha256=ZKgHMOcbeM04k0yse83TzO8HxjYa36LU8j4wTET3bvg,1584 | |||
cozmoclad/clad/externalInterface/messageShared.py,sha256=jCGlosq1IO-ozr7CTCU2Ksf9A18Qs76X0gHOWN_QuL8,28732 | |||
cozmoclad/clad/externalInterface/messageToBehaviorManager.py,sha256=M4Ap5O3bz884X1BIecRAsRv3-9OSHgEpHV1Oz0HlteY,17470 | |||
cozmoclad/clad/physicsInterface/__init__.py,sha256=M-ndufPpUa5HCGUZpUi3XeMvb5T1rM3xZhf0stXJ0BQ,611 | |||
cozmoclad/clad/physicsInterface/messageSimPhysics.py,sha256=bfffRCRxtCxj3h6SQdJWAtn7fRGB07zHJAxlMebYIL8,9943 | |||
cozmoclad/clad/robotInterface/__init__.py,sha256=M-ndufPpUa5HCGUZpUi3XeMvb5T1rM3xZhf0stXJ0BQ,611 | |||
cozmoclad/clad/robotInterface/bleMessages.py,sha256=BQl5FQaXAFIhLsyxdfsXsg2LTvwspeSO-0SKezegYT0,12819 | |||
cozmoclad/clad/robotInterface/messageFromActiveObject.py,sha256=eFtLLn4fQxVEMN8H6Sg__tMWgSAfv6pQD_GAFGGvvVM,39298 | |||
cozmoclad/clad/robotInterface/messageToActiveObject.py,sha256=rs4lcNTlSau8PVSzORjPCbiXQVZbQBKOVI25A7rLfXg,20396 | |||
cozmoclad/clad/types/__init__.py,sha256=M-ndufPpUa5HCGUZpUi3XeMvb5T1rM3xZhf0stXJ0BQ,611 | |||
cozmoclad/clad/types/actionResults.py,sha256=XLpPtbnil21_2TkdbdaVGTpffCh349cBCHcdKlJSEeo,6284 | |||
cozmoclad/clad/types/actionTypes.py,sha256=lvdTURHBUwiCgJB5ZjeslQNf2FUd7npQi1G_60KNOUw,32608 | |||
cozmoclad/clad/types/activeObjectAccel.py,sha256=dupTDw7Uy2oNvB1Nj1y_J3r30Zsdl6xMUWbjYKBTi9c,4426 | |||
cozmoclad/clad/types/activeObjectConstants.py,sha256=BtOBNm5_6RhFbARxUhAPX6kllpA3yMUZwAZFneCOqTQ,1912 | |||
cozmoclad/clad/types/advertisementTypes.py,sha256=4Qz2GxWPe5gryTS-osivS8BGBMVDkQ9UOqgoYHZpi8I,10965 | |||
cozmoclad/clad/types/animationEvents.py,sha256=vaBLqQlFKMToBuGmRZIQhIkwhz5xjwEblaJ9Be0kqOI,1714 | |||
cozmoclad/clad/types/animationTrigger.py,sha256=Ed38MpW95bizGbnlyEazGGCERXWSHdx3Kn4kK_koQDQ,30863 | |||
cozmoclad/clad/types/birthCertificate.py,sha256=ggglw4-V3EauQyY3oT8PuxRQSM3QdjtgUNjUesiMKXQ,8576 | |||
cozmoclad/clad/types/cladPoint.py,sha256=V68j_0VCdUrS-79jjlKN_XoBkxeoQR3Zx4sneNW1kKo,6381 | |||
cozmoclad/clad/types/cladRect.py,sha256=y69pB5ZUnGJqN_Y0JaU3L1n_1fZfq-mSxTSkRkcRgvk,5134 | |||
cozmoclad/clad/types/controllerChannels.py,sha256=uueRgE15uSC9tR2mezDHpO5TaAKmoakL4Drc6RTvx3g,1779 | |||
cozmoclad/clad/types/customObjectMarkers.py,sha256=Tcm2V3nc3PXk_Avp63nBZ0dWm2SG7vXDRwxybSMkFqw,2043 | |||
cozmoclad/clad/types/debugConsoleTypes.py,sha256=Pmd-q3rYpyNuYEA0Cp0-FdAjlOAC0R81DKNxhKV9Wo0,8827 | |||
cozmoclad/clad/types/deviceDataTypes.py,sha256=J5Nc_5_uAVdmLcB5JpAnmr2m3nFMWsZNjKrOgu9Dymg,4647 | |||
cozmoclad/clad/types/emotionTypes.py,sha256=II6fbhk-XkkokyxIMczN4gVORFDfWRoKxG44HJn38rE,1875 | |||
cozmoclad/clad/types/engineErrorCodes.py,sha256=Ac8QQcPwoTUqlVrqxbM0fgmpHbe2HZqJ14SwWUg-l0s,1953 | |||
cozmoclad/clad/types/engineState.py,sha256=DS4rJjALzLNcAQG2QG6do-uvm40kcIZ9VD-fuKQ-lu8,2785 | |||
cozmoclad/clad/types/enrolledFaceStorage.py,sha256=osfy5ku0DFoeWNRPjzu106czKpZUUR3lantUGIeoBFE,8483 | |||
cozmoclad/clad/types/faceDetectionMetaData.py,sha256=zuo0tIybRjd8jdEOKMp2ApcdJVPrA2bc-1vZa7Gv2qs,11766 | |||
cozmoclad/clad/types/faceEnrollmentPoses.py,sha256=6xMSCjldB4LuCyrGTeu25w08DNuT-7jn21Zi8lZ5QRE,1878 | |||
cozmoclad/clad/types/faceEnrollmentResult.py,sha256=imSkYEk25u_d5AqWtjaW0N-RXAEJsI4qrIM4L2JBptw,1941 | |||
cozmoclad/clad/types/facialExpressions.py,sha256=FZljp_a_JdwbQ-jL4fEdEEkUDP1dvVpRDYZMHfObX88,1762 | |||
cozmoclad/clad/types/factoryTestTypes.py,sha256=sckYUfy-kbeTGFFHYVx3tTxNCF1P9bvfj6P7DIEDgU0,22497 | |||
cozmoclad/clad/types/featureGateTypes.py,sha256=Kj2S6eixE5kb8OTbdbQH7JX3Y7no6EMwDZ28t3LSBoc,4905 | |||
cozmoclad/clad/types/firmwareTypes.py,sha256=ekipKb8fQusfZJGuokZKFzZ-99Mt4TcRfTqiE2dwCSc,2556 | |||
cozmoclad/clad/types/gameStatusFlag.py,sha256=aVLHRSkgOyDU0EBr3vVY6Xka4nJ-w42YadbuyHfAwGs,1761 | |||
cozmoclad/clad/types/globalVizOrigin.py,sha256=0J9NRALx-76hAnENk-4D3BApcy1xS6asR1cteMCZm_8,7461 | |||
cozmoclad/clad/types/imageTypes.py,sha256=l095cNxzJ4VsKCC1JqtrgEnexPtqlquSOapOFj_l3Vo,21319 | |||
cozmoclad/clad/types/imu.py,sha256=C1AM1LXQnGKwWWTKERKF5SpmdzRaxpvQm1TTG_H6uic,22124 | |||
cozmoclad/clad/types/inventoryTypes.py,sha256=cKll-bIlfI_LKrJbtuFDy9zhJgbUat54pMfy5MgMlv0,5970 | |||
cozmoclad/clad/types/keyWords.py,sha256=CRuki50JP2GQQ8VG0kdFgfXIyJlNKM6vkmadSdx_ld8,1801 | |||
cozmoclad/clad/types/ledTypes.py,sha256=nUGpJS-Ez_m_-n_hymt4FKTKDHnlyw8iPdVAyjRu8Ok,9831 | |||
cozmoclad/clad/types/liveIdleAnimationParameters.py,sha256=qbN2WKVJoonIT5otPW5exyFUYh_Yi_7CJs-99bbj6BE,2901 | |||
cozmoclad/clad/types/loadedKnownFace.py,sha256=qGhwVoUIq6rd-G8hHSAvf_oyfryWr_W5r2YNRSaGMSc,9807 | |||
cozmoclad/clad/types/logLevels.py,sha256=HZ1gfB_16cqwa4U_lUSDgtqqvXnT5xSiqbcMqSxjKfM,1757 | |||
cozmoclad/clad/types/memoryMap.py,sha256=0SfWm38WsqIxnqXn5o5lf3nOYfvYPJZ8ByPLrGhZNuE,13608 | |||
cozmoclad/clad/types/motorTypes.py,sha256=XrVWI0DDxWMX2gzAI1HcvbChPNJv6E8pK9cUBER_x2Q,7859 | |||
cozmoclad/clad/types/needsSystemTypes.py,sha256=FEsg_TkZBV7mhqlcxDgOF-vCEL9PhnydXoAZbehrmqQ,112705 | |||
cozmoclad/clad/types/nvStorageTypes.py,sha256=YN44A1mCvFdTfXMToa4JILZ8aU-JxucutxAdzeVeQNk,4565 | |||
cozmoclad/clad/types/objectFamilies.py,sha256=XXvVZvchGoctvOt-2W11yTPBg0j4x8f8aM0DUx29PIo,1921 | |||
cozmoclad/clad/types/objectTypes.py,sha256=YcdcuFieXPMDjwy9N5q1FYJivKy7BrQcCJH46tm-CdE,5696 | |||
cozmoclad/clad/types/offTreadsStates.py,sha256=EHOau_BRRn0ZQVcRrf4vSue6O4Xxf_bSiHm6W0ZjvRc,1848 | |||
cozmoclad/clad/types/onboardingData.py,sha256=kfjxkv7RAeABLbUoc6lQgletJ4fuuVwRf_OrmqSTcLk,3974 | |||
cozmoclad/clad/types/pathEventTypes.py,sha256=1oUI4knbhYH4CgGGfTQzO8DQEfTK8E1NQqNEaXeOQoM,1689 | |||
cozmoclad/clad/types/pathMotionProfile.py,sha256=hC3IlgjZMazu4vUWjslO4qo2SLcWVz-pKRJQA8O6l6Q,12127 | |||
cozmoclad/clad/types/petTypes.py,sha256=3saRaWHaglnVfO-0jvDBtMGMhk0uqcXpZshwV5NI0jQ,1638 | |||
cozmoclad/clad/types/poseStructs.py,sha256=WoQDE_GN6XT2lTUTnNGfLljI205o6Spk9VgK1mjeX_c,6731 | |||
cozmoclad/clad/types/proceduralEyeParameters.py,sha256=lCL-Lq8qm7uTRmkZbcBSP45C7ZWfQHXr4xGWbAM_gD4,2261 | |||
cozmoclad/clad/types/proxMessages.py,sha256=H9rHBfMFhscS0uJNBTm5gxM6sLpeCW5CBowXnzeIu3w,6702 | |||
cozmoclad/clad/types/robotPublicState.py,sha256=VqP8FKWIRcSK69YH7l8MVSld5IjnTsCF1krQ-9apex4,20252 | |||
cozmoclad/clad/types/robotStatusAndActions.py,sha256=TF3qHoWzx78YW4lWhipap_IeLZpGe5xBmwHw7_7rpU0,37527 | |||
cozmoclad/clad/types/robotTestModes.py,sha256=Ff9WBdMZWus-fWgPNFPU2Mr9Bfpn3DpfiH3kPKk-qwg,8755 | |||
cozmoclad/clad/types/sayTextStyles.py,sha256=L0HnGApzgpLenV9dYVjLOgM9ZN7fqiBcoqfD4EKH1xY,2236 | |||
cozmoclad/clad/types/sdkStatusTypes.py,sha256=HuQ8dxBJ-cruEYpSfZz0-gqKqoJXyPId9Wv8MO8qjxw,1762 | |||
cozmoclad/clad/types/simpleMoodTypes.py,sha256=VhsLqMkULxubomEy8EDY3fnKnBxS__-XXWOJsU30t54,1779 | |||
cozmoclad/clad/types/toolCodes.py,sha256=Il3Axtqo-J3dxnlrLKejo3vKXzzOL2MOC5am_gAOJlA,11439 | |||
cozmoclad/clad/types/uiConnectionTypes.py,sha256=83FOjOtNMDuPV8p36zsHLE-D6zahgIXTgBLMXYbHCh8,1803 | |||
cozmoclad/clad/types/unexpectedMovementTypes.py,sha256=dL5r_UYLc0AksWyLhZ4yBLz1pdvMh5V_h7RQPz3cZ0M,2121 | |||
cozmoclad/clad/types/unlockTypes.py,sha256=ANT4EbWd40vVK30GAvio2RzJTF8mKWa3Ej_5O9V3vE0,7117 | |||
cozmoclad/clad/types/userFacingResults.py,sha256=Po-a4G2LX1x0F03yRHeNKgkDtGMfisKJ_b7feDTGCmU,1903 | |||
cozmoclad/clad/types/visionModes.py,sha256=kLa9MpUOkPpCj2jFzq1xFOJM56Xx46HGD7Mt_PbbeO8,2366 | |||
cozmoclad/clad/types/vizTypes.py,sha256=udo9_6VTge4bSFHseSHLv27a7wtD6qu0AQCyn7qr4KU,3105 | |||
cozmoclad/clad/types/behaviorSystem/__init__.py,sha256=M-ndufPpUa5HCGUZpUi3XeMvb5T1rM3xZhf0stXJ0BQ,611 | |||
cozmoclad/clad/types/behaviorSystem/activityTypes.py,sha256=SCMQ6vtZaA0WGruenNzqM_O9gSgpZXw0zN_4JUNxg_k,3723 | |||
cozmoclad/clad/types/behaviorSystem/behaviorChooserTypes.py,sha256=w3Eb2Du9yMU79KJJ4KyQpgokEe7-fceimchfcAULk_s,1852 | |||
cozmoclad/clad/types/behaviorSystem/behaviorObjectives.py,sha256=Jr8DPXnBqqpd1QUMS4P0p8l36axicCgnbZye8Ojg1JA,3279 | |||
cozmoclad/clad/types/behaviorSystem/behaviorTypes.py,sha256=51iKLykvSIe_KBWfAxMXG0NyUHV2RB1Qy8pIw0IKFlw,13645 | |||
cozmoclad/clad/types/behaviorSystem/reactionTriggers.py,sha256=FSraQhbnPqr6y6SJ16FZv0LCzQb7P_0TtZkKFGVWhKU,22469 | |||
cozmoclad/clad/types/behaviorSystem/strategyTypes.py,sha256=r7otJBGq9prYSTFuCiTkZEqg0XI_Boti35R2YjkB4ww,2048 | |||
cozmoclad/clad/vizInterface/__init__.py,sha256=M-ndufPpUa5HCGUZpUi3XeMvb5T1rM3xZhf0stXJ0BQ,611 | |||
cozmoclad/clad/vizInterface/messageViz.py,sha256=Ch9xg8MHNc_fU31V_kbtffzdRu0mNPdmGpju057KNwo,235851 | |||
cozmoclad/msgbuffers/__init__.py,sha256=yHU78-ajX5MMztb_lWnRoUrykfs1bd6JMNTK9XhQFeE,15154 | |||
cozmoclad/util/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 | |||
cozmoclad/util/ankiLab/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 | |||
cozmoclad/util/ankiLab/ankiLabDef.py,sha256=DbSrc8YL0rgLcl_hkaHylrRRzvTpUc4-thIMqn_m6bc,34333 | |||
cozmoclad-3.4.0.dist-info/LICENSE-header-cs.txt,sha256=1ePfQJNyWgeiQ6ErW3zkSILtZ_ye26YuEuGHYUTFr2k,624 | |||
cozmoclad-3.4.0.dist-info/LICENSE-header-py.txt,sha256=M-ndufPpUa5HCGUZpUi3XeMvb5T1rM3xZhf0stXJ0BQ,611 | |||
cozmoclad-3.4.0.dist-info/LICENSE.txt,sha256=THPe12iw4yd8mbwjmhwsGiCsmC2QDopJnGNSbSh9hkc,10356 | |||
cozmoclad-3.4.0.dist-info/METADATA,sha256=Yp6ASuejzwmCu69FO2PNrs3piVYH_B09iD1U5PlNWh8,661 | |||
cozmoclad-3.4.0.dist-info/WHEEL,sha256=MYFsq5fFBwF_oyJgrOoFmYYB1K6Sw7MxY-0897ZLbdM,92 | |||
cozmoclad-3.4.0.dist-info/top_level.txt,sha256=dc2K3FhXEpduEs9V_ryWNr1-xf9jwJTSXoPJTHHUPM0,10 | |||
cozmoclad-3.4.0.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1 | |||
cozmoclad-3.4.0.dist-info/RECORD,, | |||
cozmoclad-3.4.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 | |||
cozmoclad/clad/audio/__pycache__/audioBusTypes.cpython-36.pyc,, | |||
cozmoclad/clad/audio/__pycache__/audioCallbackMessage.cpython-36.pyc,, | |||
cozmoclad/clad/audio/__pycache__/audioEventTypes.cpython-36.pyc,, | |||
cozmoclad/clad/audio/__pycache__/audioGameObjectTypes.cpython-36.pyc,, | |||
cozmoclad/clad/audio/__pycache__/audioMessage.cpython-36.pyc,, | |||
cozmoclad/clad/audio/__pycache__/audioMessageTypes.cpython-36.pyc,, | |||
cozmoclad/clad/audio/__pycache__/audioParameterTypes.cpython-36.pyc,, | |||
cozmoclad/clad/audio/__pycache__/audioSoundbanks.cpython-36.pyc,, | |||
cozmoclad/clad/audio/__pycache__/audioStateTypes.cpython-36.pyc,, | |||
cozmoclad/clad/audio/__pycache__/audioSwitchTypes.cpython-36.pyc,, | |||
cozmoclad/clad/audio/__pycache__/messageAudioClient.cpython-36.pyc,, | |||
cozmoclad/clad/audio/__pycache__/__init__.cpython-36.pyc,, | |||
cozmoclad/clad/externalInterface/__pycache__/messageActions.cpython-36.pyc,, | |||
cozmoclad/clad/externalInterface/__pycache__/messageEngineToGame.cpython-36.pyc,, | |||
cozmoclad/clad/externalInterface/__pycache__/messageEngineToGame_hash.cpython-36.pyc,, | |||
cozmoclad/clad/externalInterface/__pycache__/messageGameToEngine.cpython-36.pyc,, | |||
cozmoclad/clad/externalInterface/__pycache__/messageGameToEngine_hash.cpython-36.pyc,, | |||
cozmoclad/clad/externalInterface/__pycache__/messageShared.cpython-36.pyc,, | |||
cozmoclad/clad/externalInterface/__pycache__/messageToBehaviorManager.cpython-36.pyc,, | |||
cozmoclad/clad/externalInterface/__pycache__/__init__.cpython-36.pyc,, | |||
cozmoclad/clad/physicsInterface/__pycache__/messageSimPhysics.cpython-36.pyc,, | |||
cozmoclad/clad/physicsInterface/__pycache__/__init__.cpython-36.pyc,, | |||
cozmoclad/clad/robotInterface/__pycache__/bleMessages.cpython-36.pyc,, | |||
cozmoclad/clad/robotInterface/__pycache__/messageFromActiveObject.cpython-36.pyc,, | |||
cozmoclad/clad/robotInterface/__pycache__/messageToActiveObject.cpython-36.pyc,, | |||
cozmoclad/clad/robotInterface/__pycache__/__init__.cpython-36.pyc,, | |||
cozmoclad/clad/types/behaviorSystem/__pycache__/activityTypes.cpython-36.pyc,, | |||
cozmoclad/clad/types/behaviorSystem/__pycache__/behaviorChooserTypes.cpython-36.pyc,, | |||
cozmoclad/clad/types/behaviorSystem/__pycache__/behaviorObjectives.cpython-36.pyc,, | |||
cozmoclad/clad/types/behaviorSystem/__pycache__/behaviorTypes.cpython-36.pyc,, | |||
cozmoclad/clad/types/behaviorSystem/__pycache__/reactionTriggers.cpython-36.pyc,, | |||
cozmoclad/clad/types/behaviorSystem/__pycache__/strategyTypes.cpython-36.pyc,, | |||
cozmoclad/clad/types/behaviorSystem/__pycache__/__init__.cpython-36.pyc,, | |||
cozmoclad/clad/types/__pycache__/actionResults.cpython-36.pyc,, | |||
cozmoclad/clad/types/__pycache__/actionTypes.cpython-36.pyc,, | |||
cozmoclad/clad/types/__pycache__/activeObjectAccel.cpython-36.pyc,, | |||
cozmoclad/clad/types/__pycache__/activeObjectConstants.cpython-36.pyc,, | |||
cozmoclad/clad/types/__pycache__/advertisementTypes.cpython-36.pyc,, | |||
cozmoclad/clad/types/__pycache__/animationEvents.cpython-36.pyc,, | |||
cozmoclad/clad/types/__pycache__/animationTrigger.cpython-36.pyc,, | |||
cozmoclad/clad/types/__pycache__/birthCertificate.cpython-36.pyc,, | |||
cozmoclad/clad/types/__pycache__/cladPoint.cpython-36.pyc,, | |||
cozmoclad/clad/types/__pycache__/cladRect.cpython-36.pyc,, | |||
cozmoclad/clad/types/__pycache__/controllerChannels.cpython-36.pyc,, | |||
cozmoclad/clad/types/__pycache__/customObjectMarkers.cpython-36.pyc,, | |||
cozmoclad/clad/types/__pycache__/debugConsoleTypes.cpython-36.pyc,, | |||
cozmoclad/clad/types/__pycache__/deviceDataTypes.cpython-36.pyc,, | |||
cozmoclad/clad/types/__pycache__/emotionTypes.cpython-36.pyc,, | |||
cozmoclad/clad/types/__pycache__/engineErrorCodes.cpython-36.pyc,, | |||
cozmoclad/clad/types/__pycache__/engineState.cpython-36.pyc,, | |||
cozmoclad/clad/types/__pycache__/enrolledFaceStorage.cpython-36.pyc,, | |||
cozmoclad/clad/types/__pycache__/faceDetectionMetaData.cpython-36.pyc,, | |||
cozmoclad/clad/types/__pycache__/faceEnrollmentPoses.cpython-36.pyc,, | |||
cozmoclad/clad/types/__pycache__/faceEnrollmentResult.cpython-36.pyc,, | |||
cozmoclad/clad/types/__pycache__/facialExpressions.cpython-36.pyc,, | |||
cozmoclad/clad/types/__pycache__/factoryTestTypes.cpython-36.pyc,, | |||
cozmoclad/clad/types/__pycache__/featureGateTypes.cpython-36.pyc,, | |||
cozmoclad/clad/types/__pycache__/firmwareTypes.cpython-36.pyc,, | |||
cozmoclad/clad/types/__pycache__/gameStatusFlag.cpython-36.pyc,, | |||
cozmoclad/clad/types/__pycache__/globalVizOrigin.cpython-36.pyc,, | |||
cozmoclad/clad/types/__pycache__/imageTypes.cpython-36.pyc,, | |||
cozmoclad/clad/types/__pycache__/imu.cpython-36.pyc,, | |||
cozmoclad/clad/types/__pycache__/inventoryTypes.cpython-36.pyc,, | |||
cozmoclad/clad/types/__pycache__/keyWords.cpython-36.pyc,, | |||
cozmoclad/clad/types/__pycache__/ledTypes.cpython-36.pyc,, | |||
cozmoclad/clad/types/__pycache__/liveIdleAnimationParameters.cpython-36.pyc,, | |||
cozmoclad/clad/types/__pycache__/loadedKnownFace.cpython-36.pyc,, | |||
cozmoclad/clad/types/__pycache__/logLevels.cpython-36.pyc,, | |||
cozmoclad/clad/types/__pycache__/memoryMap.cpython-36.pyc,, | |||
cozmoclad/clad/types/__pycache__/motorTypes.cpython-36.pyc,, | |||
cozmoclad/clad/types/__pycache__/needsSystemTypes.cpython-36.pyc,, | |||
cozmoclad/clad/types/__pycache__/nvStorageTypes.cpython-36.pyc,, | |||
cozmoclad/clad/types/__pycache__/objectFamilies.cpython-36.pyc,, | |||
cozmoclad/clad/types/__pycache__/objectTypes.cpython-36.pyc,, | |||
cozmoclad/clad/types/__pycache__/offTreadsStates.cpython-36.pyc,, | |||
cozmoclad/clad/types/__pycache__/onboardingData.cpython-36.pyc,, | |||
cozmoclad/clad/types/__pycache__/pathEventTypes.cpython-36.pyc,, | |||
cozmoclad/clad/types/__pycache__/pathMotionProfile.cpython-36.pyc,, | |||
cozmoclad/clad/types/__pycache__/petTypes.cpython-36.pyc,, | |||
cozmoclad/clad/types/__pycache__/poseStructs.cpython-36.pyc,, | |||
cozmoclad/clad/types/__pycache__/proceduralEyeParameters.cpython-36.pyc,, | |||
cozmoclad/clad/types/__pycache__/proxMessages.cpython-36.pyc,, | |||
cozmoclad/clad/types/__pycache__/robotPublicState.cpython-36.pyc,, | |||
cozmoclad/clad/types/__pycache__/robotStatusAndActions.cpython-36.pyc,, | |||
cozmoclad/clad/types/__pycache__/robotTestModes.cpython-36.pyc,, | |||
cozmoclad/clad/types/__pycache__/sayTextStyles.cpython-36.pyc,, | |||
cozmoclad/clad/types/__pycache__/sdkStatusTypes.cpython-36.pyc,, | |||
cozmoclad/clad/types/__pycache__/simpleMoodTypes.cpython-36.pyc,, | |||
cozmoclad/clad/types/__pycache__/toolCodes.cpython-36.pyc,, | |||
cozmoclad/clad/types/__pycache__/uiConnectionTypes.cpython-36.pyc,, | |||
cozmoclad/clad/types/__pycache__/unexpectedMovementTypes.cpython-36.pyc,, | |||
cozmoclad/clad/types/__pycache__/unlockTypes.cpython-36.pyc,, | |||
cozmoclad/clad/types/__pycache__/userFacingResults.cpython-36.pyc,, | |||
cozmoclad/clad/types/__pycache__/visionModes.cpython-36.pyc,, | |||
cozmoclad/clad/types/__pycache__/vizTypes.cpython-36.pyc,, | |||
cozmoclad/clad/types/__pycache__/__init__.cpython-36.pyc,, | |||
cozmoclad/clad/vizInterface/__pycache__/messageViz.cpython-36.pyc,, | |||
cozmoclad/clad/vizInterface/__pycache__/__init__.cpython-36.pyc,, | |||
cozmoclad/clad/__pycache__/__init__.cpython-36.pyc,, | |||
cozmoclad/msgbuffers/__pycache__/__init__.cpython-36.pyc,, | |||
cozmoclad/util/ankiLab/__pycache__/ankiLabDef.cpython-36.pyc,, | |||
cozmoclad/util/ankiLab/__pycache__/__init__.cpython-36.pyc,, | |||
cozmoclad/util/__pycache__/__init__.cpython-36.pyc,, | |||
cozmoclad/__pycache__/__init__.cpython-36.pyc,, |
@@ -0,0 +1,5 @@ | |||
Wheel-Version: 1.0 | |||
Generator: bdist_wheel (0.32.2) | |||
Root-Is-Purelib: true | |||
Tag: py3-none-any | |||
@@ -0,0 +1 @@ | |||
cozmoclad |
@@ -0,0 +1 @@ | |||
@@ -0,0 +1,180 @@ | |||
Unless otherwise stated in that file, or the folder containing that file, all | |||
files in the Cozmo SDK are Copyright (c) 2016-2017 Anki Inc. and licensed under | |||
the Apache 2.0 License: | |||
Apache License | |||
Version 2.0, January 2004 | |||
http://www.apache.org/licenses/ | |||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION | |||
1. Definitions. | |||
"License" shall mean the terms and conditions for use, reproduction, | |||
and distribution as defined by Sections 1 through 9 of this document. | |||
"Licensor" shall mean the copyright owner or entity authorized by | |||
the copyright owner that is granting the License. | |||
"Legal Entity" shall mean the union of the acting entity and all | |||
other entities that control, are controlled by, or are under common | |||
control with that entity. For the purposes of this definition, | |||
"control" means (i) the power, direct or indirect, to cause the | |||
direction or management of such entity, whether by contract or | |||
otherwise, or (ii) ownership of fifty percent (50%) or more of the | |||
outstanding shares, or (iii) beneficial ownership of such entity. | |||
"You" (or "Your") shall mean an individual or Legal Entity | |||
exercising permissions granted by this License. | |||
"Source" form shall mean the preferred form for making modifications, | |||
including but not limited to software source code, documentation | |||
source, and configuration files. | |||
"Object" form shall mean any form resulting from mechanical | |||
transformation or translation of a Source form, including but | |||
not limited to compiled object code, generated documentation, | |||
and conversions to other media types. | |||
"Work" shall mean the work of authorship, whether in Source or | |||
Object form, made available under the License, as indicated by a | |||
copyright notice that is included in or attached to the work | |||
(an example is provided in the Appendix below). | |||
"Derivative Works" shall mean any work, whether in Source or Object | |||
form, that is based on (or derived from) the Work and for which the | |||
editorial revisions, annotations, elaborations, or other modifications | |||
represent, as a whole, an original work of authorship. For the purposes | |||
of this License, Derivative Works shall not include works that remain | |||
separable from, or merely link (or bind by name) to the interfaces of, | |||
the Work and Derivative Works thereof. | |||
"Contribution" shall mean any work of authorship, including | |||
the original version of the Work and any modifications or additions | |||
to that Work or Derivative Works thereof, that is intentionally | |||
submitted to Licensor for inclusion in the Work by the copyright owner | |||
or by an individual or Legal Entity authorized to submit on behalf of | |||
the copyright owner. For the purposes of this definition, "submitted" | |||
means any form of electronic, verbal, or written communication sent | |||
to the Licensor or its representatives, including but not limited to | |||
communication on electronic mailing lists, source code control systems, | |||
and issue tracking systems that are managed by, or on behalf of, the | |||
Licensor for the purpose of discussing and improving the Work, but | |||
excluding communication that is conspicuously marked or otherwise | |||
designated in writing by the copyright owner as "Not a Contribution." | |||
"Contributor" shall mean Licensor and any individual or Legal Entity | |||
on behalf of whom a Contribution has been received by Licensor and | |||
subsequently incorporated within the Work. | |||
2. Grant of Copyright License. Subject to the terms and conditions of | |||
this License, each Contributor hereby grants to You a perpetual, | |||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable | |||
copyright license to reproduce, prepare Derivative Works of, | |||
publicly display, publicly perform, sublicense, and distribute the | |||
Work and such Derivative Works in Source or Object form. | |||
3. Grant of Patent License. Subject to the terms and conditions of | |||
this License, each Contributor hereby grants to You a perpetual, | |||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable | |||
(except as stated in this section) patent license to make, have made, | |||
use, offer to sell, sell, import, and otherwise transfer the Work, | |||
where such license applies only to those patent claims licensable | |||
by such Contributor that are necessarily infringed by their | |||
Contribution(s) alone or by combination of their Contribution(s) | |||
with the Work to which such Contribution(s) was submitted. If You | |||
institute patent litigation against any entity (including a | |||
cross-claim or counterclaim in a lawsuit) alleging that the Work | |||
or a Contribution incorporated within the Work constitutes direct | |||
or contributory patent infringement, then any patent licenses | |||
granted to You under this License for that Work shall terminate | |||
as of the date such litigation is filed. | |||
4. Redistribution. You may reproduce and distribute copies of the | |||
Work or Derivative Works thereof in any medium, with or without | |||
modifications, and in Source or Object form, provided that You | |||
meet the following conditions: | |||
(a) You must give any other recipients of the Work or | |||
Derivative Works a copy of this License; and | |||
(b) You must cause any modified files to carry prominent notices | |||
stating that You changed the files; and | |||
(c) You must retain, in the Source form of any Derivative Works | |||
that You distribute, all copyright, patent, trademark, and | |||
attribution notices from the Source form of the Work, | |||
excluding those notices that do not pertain to any part of | |||
the Derivative Works; and | |||
(d) If the Work includes a "NOTICE" text file as part of its | |||
distribution, then any Derivative Works that You distribute must | |||
include a readable copy of the attribution notices contained | |||
within such NOTICE file, excluding those notices that do not | |||
pertain to any part of the Derivative Works, in at least one | |||
of the following places: within a NOTICE text file distributed | |||
as part of the Derivative Works; within the Source form or | |||
documentation, if provided along with the Derivative Works; or, | |||
within a display generated by the Derivative Works, if and | |||
wherever such third-party notices normally appear. The contents | |||
of the NOTICE file are for informational purposes only and | |||
do not modify the License. You may add Your own attribution | |||
notices within Derivative Works that You distribute, alongside | |||
or as an addendum to the NOTICE text from the Work, provided | |||
that such additional attribution notices cannot be construed | |||
as modifying the License. | |||
You may add Your own copyright statement to Your modifications and | |||
may provide additional or different license terms and conditions | |||
for use, reproduction, or distribution of Your modifications, or | |||
for any such Derivative Works as a whole, provided Your use, | |||
reproduction, and distribution of the Work otherwise complies with | |||
the conditions stated in this License. | |||
5. Submission of Contributions. Unless You explicitly state otherwise, | |||
any Contribution intentionally submitted for inclusion in the Work | |||
by You to the Licensor shall be under the terms and conditions of | |||
this License, without any additional terms or conditions. | |||
Notwithstanding the above, nothing herein shall supersede or modify | |||
the terms of any separate license agreement you may have executed | |||
with Licensor regarding such Contributions. | |||
6. Trademarks. This License does not grant permission to use the trade | |||
names, trademarks, service marks, or product names of the Licensor, | |||
except as required for reasonable and customary use in describing the | |||
origin of the Work and reproducing the content of the NOTICE file. | |||
7. Disclaimer of Warranty. Unless required by applicable law or | |||
agreed to in writing, Licensor provides the Work (and each | |||
Contributor provides its Contributions) on an "AS IS" BASIS, | |||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | |||
implied, including, without limitation, any warranties or conditions | |||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A | |||
PARTICULAR PURPOSE. You are solely responsible for determining the | |||
appropriateness of using or redistributing the Work and assume any | |||
risks associated with Your exercise of permissions under this License. | |||
8. Limitation of Liability. In no event and under no legal theory, | |||
whether in tort (including negligence), contract, or otherwise, | |||
unless required by applicable law (such as deliberate and grossly | |||
negligent acts) or agreed to in writing, shall any Contributor be | |||
liable to You for damages, including any direct, indirect, special, | |||
incidental, or consequential damages of any character arising as a | |||
result of this License or out of the use or inability to use the | |||
Work (including but not limited to damages for loss of goodwill, | |||
work stoppage, computer failure or malfunction, or any and all | |||
other commercial damages or losses), even if such Contributor | |||
has been advised of the possibility of such damages. | |||
9. Accepting Warranty or Additional Liability. While redistributing | |||
the Work or Derivative Works thereof, You may choose to offer, | |||
and charge a fee for, acceptance of support, warranty, indemnity, | |||
or other liability obligations and/or rights consistent with this | |||
License. However, in accepting such obligations, You may act only | |||
on Your own behalf and on Your sole responsibility, not on behalf | |||
of any other Contributor, and only if You agree to indemnify, | |||
defend, and hold each Contributor harmless for any liability | |||
incurred by, or claims asserted against, such Contributor by reason | |||
of your accepting any such warranty or additional liability. | |||
END OF TERMS AND CONDITIONS |
@@ -0,0 +1,74 @@ | |||
# Copyright (c) 2016 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. | |||
__version__ = "3.4.0" | |||
# build version string, to match the one in the app engine | |||
__build_version__ = "00003.00004.00000" | |||
class CLADHashMismatch(Exception): | |||
'''Raised by assert_clad_match if the supplied CLAD hashes do not match''' | |||
def assert_hash_match(first, second, check_name): | |||
'''Compare two CLAD hashes for equality | |||
Args: | |||
first (int, list of ints or bytes): First hash to compare | |||
second (int, list of ints or bytes): Second hash to compare | |||
check_name (string): A description to put in a raised exception message | |||
Returns: | |||
True if the hashes match exactly | |||
Raises: | |||
CLADHashMismatch if the hashes do not match. | |||
''' | |||
def normalize(h): | |||
if isinstance(h, bytes): | |||
return h | |||
if isinstance(h, list): | |||
return bytes(h) | |||
if isinstance(h, int): | |||
return h.to_bytes(16, byteorder='little') | |||
raise TypeError('Invalid hash type %s for value %s' % (type(h), h)) | |||
first = normalize(first) | |||
second = normalize(second) | |||
if first != second: | |||
raise CLADHashMismatch("CLAD version mismatch (%s) %s != %s" % (check_name, first.hex(), second.hex())) | |||
return True | |||
def assert_clad_match(to_game_hash, to_engine_hash): | |||
'''Assert that the supplied CLAD hashes match those in this package. | |||
Args: | |||
to_game_hash (int, list of ints or bytes): The expected hash for the | |||
message engine to game interface | |||
to_engine_hash (int, list of ints or bytes): The expected hash for the | |||
message game to interface interface | |||
Returns: | |||
True if the hashes match exactly | |||
Raises: | |||
CLADHashMismatch if the hashes do not match. | |||
''' | |||
from .clad.externalInterface.messageEngineToGame_hash import messageEngineToGameHash as clad_to_game_hash | |||
from .clad.externalInterface.messageGameToEngine_hash import messageGameToEngineHash as clad_to_engine_hash | |||
assert_hash_match(to_game_hash, clad_to_game_hash, 'to_game') | |||
assert_hash_match(to_engine_hash, clad_to_engine_hash, 'to_engine') | |||
return True |
@@ -0,0 +1,14 @@ | |||
# 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. | |||
@@ -0,0 +1,14 @@ | |||
# 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. | |||