├── pycozmo ├── tests │ ├── __init__.py │ ├── test_decay_graph.py │ ├── test_robot_debug.py │ ├── test_frame.py │ └── test_conn.py ├── audiokinetic │ ├── tests │ │ ├── __init__.py │ │ ├── test_soundbanksinfo.py │ │ └── test_soundbank.py │ ├── wem.py │ ├── __init__.py │ ├── exception.py │ └── soundbanksinfo.py ├── expressions │ └── __init__.py ├── CozmoAnim │ ├── __init__.py │ ├── RecordHeading.py │ ├── Event.py │ ├── FaceAnimation.py │ ├── AnimClip.py │ ├── AnimClips.py │ ├── BodyMotion.py │ ├── HeadAngle.py │ ├── LiftHeight.py │ ├── cozmo_anim.fbs │ ├── RobotAudio.py │ ├── TurnToRecordedHeading.py │ ├── ProceduralFace.py │ └── BackpackLights.py ├── object.py ├── filter.py ├── exception.py ├── logger.py ├── json_loader.py ├── __init__.py ├── audio.py ├── run.py ├── lights.py ├── protocol_base.py ├── emotions.py ├── behavior.py ├── robot.py ├── event.py ├── window.py ├── frame.py └── camera.py ├── .github ├── FUNDING.yml └── workflows │ └── pythonpackage.yml ├── requirements-dev.txt ├── assets └── pycozmo.png ├── examples ├── hello.wav ├── minimal.py ├── procedural_face_show.py ├── go_to_pose.py ├── audio.py ├── server.py ├── extremes.py ├── backpack_lights.py ├── client.py ├── anim.py ├── display_image.py ├── camera.py ├── imu.py ├── procedural_face.py ├── nvram.py ├── cube_lights.py ├── charger_lights.py ├── video.py ├── cube_light_animation.py ├── display_lines.py ├── path.py ├── procedural_face_expressions.py └── events.py ├── requirements.txt ├── .flake8 ├── .gitignore ├── tools ├── pycozmo_protocol_generator.py ├── pycozmo_app.py ├── pycozmo_replay.py ├── pycozmo_dump.py ├── pycozmo_resources.py ├── pycozmo_anim.py └── pycozmo_update.py ├── docs ├── source │ ├── index.rst │ ├── Makefile │ ├── make.bat │ ├── api.rst │ ├── conf.py │ └── overview.md ├── esp8266.md ├── hardware_versions.md ├── offboard_functions.md └── versions.md ├── mypy.ini ├── CONTRIBUTING.md ├── LICENSE.md ├── NOTICE ├── setup.py └── CHANGES.md /pycozmo/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pycozmo/audiokinetic/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | 2 | github: [zayfod] 3 | -------------------------------------------------------------------------------- /pycozmo/expressions/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | from .expressions import * # noqa 3 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | pytest 2 | flake8 3 | mypy 4 | Sphinx 5 | recommonmark 6 | -------------------------------------------------------------------------------- /assets/pycozmo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zayfod/pycozmo/HEAD/assets/pycozmo.png -------------------------------------------------------------------------------- /examples/hello.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zayfod/pycozmo/HEAD/examples/hello.wav -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | setuptools>=38.6.0 2 | dpkt 3 | numpy 4 | Pillow>=6.0.0 5 | flatbuffers 6 | -------------------------------------------------------------------------------- /pycozmo/audiokinetic/wem.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | AudioKinetic WWise WEM file representation and reading. 4 | 5 | """ 6 | -------------------------------------------------------------------------------- /examples/minimal.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import time 4 | 5 | import pycozmo 6 | 7 | 8 | with pycozmo.connect() as cli: 9 | 10 | while True: 11 | time.sleep(0.1) 12 | -------------------------------------------------------------------------------- /pycozmo/audiokinetic/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | from . import exception # noqa 3 | from . import soundbank # noqa 4 | from . import soundbanksinfo # noqa 5 | from . import wem # noqa 6 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 120 3 | exclude = pycozmo/CozmoAnim/,.github/,venv/,build/,dist/,*.egg-info/ 4 | per-file-ignores = 5 | pycozmo/__init__.py:F401,F403,F405 6 | pycozmo/protocol_declaration.py:F403,F405 7 | -------------------------------------------------------------------------------- /examples/procedural_face_show.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import pycozmo 4 | 5 | 6 | # Render a 128x64 procedural face with default parameters. 7 | face = pycozmo.procedural_face.ProceduralFace() 8 | im = face.render() 9 | im.show() 10 | -------------------------------------------------------------------------------- /examples/go_to_pose.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import pycozmo 4 | 5 | 6 | with pycozmo.connect() as cli: 7 | 8 | target = pycozmo.util.Pose(200, 100.0, 0.0, angle_z=pycozmo.util.Angle(degrees=0.0)) 9 | cli.go_to_pose(target, relative_to_robot=True) 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | .DS_Store 3 | 4 | __pycache__/ 5 | *.pyc 6 | 7 | /build/ 8 | /dist/ 9 | *.egg-info/ 10 | 11 | /venv/ 12 | /.idea/ 13 | 14 | /docs/source/build/ 15 | /docs/source/generated/ 16 | /docs/source/external/ 17 | 18 | /.mypy_cache/ 19 | 20 | /examples/*.png 21 | -------------------------------------------------------------------------------- /examples/audio.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import pycozmo 4 | 5 | 6 | with pycozmo.connect() as cli: 7 | 8 | # Set volume to ~75%. 9 | cli.set_volume(50000) 10 | 11 | # A 22 kHz, 16-bit, mono file is required. 12 | cli.play_audio("hello.wav") 13 | cli.wait_for(pycozmo.event.EvtAudioCompleted) 14 | -------------------------------------------------------------------------------- /examples/server.py: -------------------------------------------------------------------------------- 1 | 2 | import time 3 | 4 | import pycozmo 5 | 6 | 7 | pycozmo.setup_basic_logging(log_level="DEBUG", protocol_log_level="DEBUG") 8 | 9 | conn = pycozmo.conn.Connection(server=True) 10 | conn.start() 11 | 12 | while True: 13 | try: 14 | time.sleep(0.1) 15 | except KeyboardInterrupt: 16 | break 17 | 18 | conn.stop() 19 | -------------------------------------------------------------------------------- /pycozmo/CozmoAnim/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | from . import AnimClip 3 | from . import AnimClips 4 | from . import BackpackLights 5 | from . import BodyMotion 6 | from . import Event 7 | from . import FaceAnimation 8 | from . import HeadAngle 9 | from . import Keyframes 10 | from . import LiftHeight 11 | from . import ProceduralFace 12 | from . import RecordHeading 13 | from . import RobotAudio 14 | from . import TurnToRecordedHeading 15 | -------------------------------------------------------------------------------- /tools/pycozmo_protocol_generator.py: -------------------------------------------------------------------------------- 1 | 2 | from io import StringIO 3 | import shutil 4 | import pycozmo 5 | 6 | 7 | def main(): 8 | buf = StringIO() 9 | gen = pycozmo.protocol_generator.ProtocolGenerator(buf) 10 | gen.generate() 11 | 12 | buf.seek(0) 13 | with open("../pycozmo/protocol_encoder.py", "w") as f: 14 | shutil.copyfileobj(buf, f) 15 | 16 | 17 | if __name__ == '__main__': 18 | main() 19 | -------------------------------------------------------------------------------- /pycozmo/object.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | Cozmo objects (cubes, platforms, etc.). 4 | 5 | """ 6 | 7 | from . import protocol_encoder 8 | 9 | 10 | __all__ = [ 11 | "Object", 12 | ] 13 | 14 | 15 | class Object(object): 16 | """ Object representation. """ 17 | 18 | def __init__(self, factory_id: int, object_type: protocol_encoder.ObjectType) -> None: 19 | self.factory_id = factory_id 20 | self.object_type = object_type 21 | -------------------------------------------------------------------------------- /examples/extremes.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import time 4 | 5 | import pycozmo 6 | 7 | 8 | with pycozmo.connect() as cli: 9 | 10 | cli.set_head_angle(pycozmo.MAX_HEAD_ANGLE.radians) 11 | time.sleep(1) 12 | cli.set_head_angle(pycozmo.MIN_HEAD_ANGLE.radians) 13 | time.sleep(1) 14 | 15 | cli.set_lift_height(pycozmo.MAX_LIFT_HEIGHT.mm) 16 | time.sleep(1) 17 | cli.set_lift_height(pycozmo.MIN_LIFT_HEIGHT.mm) 18 | time.sleep(1) 19 | -------------------------------------------------------------------------------- /examples/backpack_lights.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import time 4 | 5 | import pycozmo 6 | 7 | 8 | with pycozmo.connect() as cli: 9 | 10 | lights = [ 11 | pycozmo.lights.red_light, 12 | pycozmo.lights.green_light, 13 | pycozmo.lights.blue_light, 14 | pycozmo.lights.white_light, 15 | pycozmo.lights.off_light, 16 | ] 17 | 18 | for light in lights: 19 | cli.set_all_backpack_lights(light) 20 | time.sleep(2) 21 | -------------------------------------------------------------------------------- /examples/client.py: -------------------------------------------------------------------------------- 1 | 2 | import time 3 | 4 | import pycozmo 5 | 6 | 7 | pycozmo.setup_basic_logging(log_level="DEBUG", protocol_log_level="DEBUG") 8 | 9 | conn = pycozmo.conn.Connection(("127.0.0.1", 5551)) 10 | conn.start() 11 | conn.connect() 12 | 13 | for i in range(100): 14 | conn.send(pycozmo.protocol_encoder.SetRobotVolume(i)) 15 | 16 | while True: 17 | try: 18 | time.sleep(0.1) 19 | except KeyboardInterrupt: 20 | break 21 | 22 | conn.disconnect() 23 | conn.stop() 24 | -------------------------------------------------------------------------------- /examples/anim.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import time 4 | 5 | import pycozmo 6 | 7 | 8 | with pycozmo.connect() as cli: 9 | 10 | # Load animations - one time. 11 | cli.load_anims() 12 | 13 | # Print the names of all available animations. 14 | names = cli.get_anim_names() 15 | for name in sorted(names): 16 | print(name) 17 | 18 | time.sleep(2) 19 | 20 | # Play an animation. 21 | cli.play_anim("anim_launch_wakeup_01") 22 | cli.wait_for(pycozmo.event.EvtAnimationCompleted) 23 | -------------------------------------------------------------------------------- /examples/display_image.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os 4 | import time 5 | 6 | from PIL import Image 7 | 8 | import pycozmo 9 | 10 | 11 | with pycozmo.connect() as cli: 12 | 13 | # Raise head. 14 | angle = (pycozmo.robot.MAX_HEAD_ANGLE.radians - pycozmo.robot.MIN_HEAD_ANGLE.radians) / 2.0 15 | cli.set_head_angle(angle) 16 | time.sleep(1) 17 | 18 | # Load image 19 | im = Image.open(os.path.join(os.path.dirname(__file__), "..", "assets", "pycozmo.png")) 20 | # Convert to binary image. 21 | im = im.convert('1') 22 | 23 | cli.display_image(im, 5.0) 24 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | 2 | PyCozmo 3 | ======= 4 | 5 | .. toctree:: 6 | :maxdepth: 2 7 | :caption: Documentation 8 | 9 | overview.md 10 | external/architecture.md 11 | external/protocol.md 12 | external/capturing.md 13 | external/functions.md 14 | external/offboard_functions.md 15 | external/versions.md 16 | external/hardware_versions.md 17 | external/esp8266.md 18 | 19 | .. toctree:: 20 | :maxdepth: 2 21 | :caption: API Reference 22 | 23 | api.rst 24 | 25 | 26 | Indices and tables 27 | ------------------ 28 | 29 | * :ref:`genindex` 30 | * :ref:`modindex` 31 | * :ref:`search` 32 | -------------------------------------------------------------------------------- /pycozmo/audiokinetic/exception.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | AudioKinetic WWise exceptions. 4 | 5 | """ 6 | 7 | import pycozmo 8 | 9 | 10 | __all__ = [ 11 | "AudioKineticBaseError", 12 | "AudioKineticFormatError", 13 | "AudioKineticIOError", 14 | ] 15 | 16 | 17 | class AudioKineticBaseError(pycozmo.exception.PyCozmoException): 18 | """ AudioKinetic WWise base error. """ 19 | pass 20 | 21 | 22 | class AudioKineticFormatError(AudioKineticBaseError): 23 | """ Invalid file format error. """ 24 | pass 25 | 26 | 27 | class AudioKineticIOError(AudioKineticBaseError): 28 | """ File I/O error. """ 29 | pass 30 | -------------------------------------------------------------------------------- /examples/camera.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import time 4 | 5 | import pycozmo 6 | 7 | 8 | def on_camera_image(cli, image): 9 | image.save("camera.png", "PNG") 10 | 11 | 12 | with pycozmo.connect() as cli: 13 | 14 | # Raise head. 15 | angle = (pycozmo.robot.MAX_HEAD_ANGLE.radians - pycozmo.robot.MIN_HEAD_ANGLE.radians) / 2.0 16 | cli.set_head_angle(angle) 17 | 18 | cli.enable_camera(enable=True, color=True) 19 | 20 | # Wait for image to stabilize. 21 | time.sleep(2.0) 22 | 23 | cli.add_handler(pycozmo.event.EvtNewRawCameraImage, on_camera_image, one_shot=True) 24 | 25 | # Wait for image to be captured. 26 | time.sleep(1) 27 | -------------------------------------------------------------------------------- /mypy.ini: -------------------------------------------------------------------------------- 1 | 2 | [mypy] 3 | python_version = 3.6 4 | 5 | # Strict mode 6 | warn_unused_configs=True 7 | #disallow_any_generics=True 8 | disallow_subclassing_any=True 9 | #disallow_untyped_calls=True 10 | #disallow_untyped_defs=True 11 | disallow_incomplete_defs=True 12 | check_untyped_defs=True 13 | disallow_untyped_decorators=True 14 | no_implicit_optional=True 15 | warn_redundant_casts=True 16 | warn_unused_ignores=True 17 | warn_return_any=True 18 | #no_implicit_reexport=True 19 | 20 | [mypy-setuptools] 21 | ignore_missing_imports = True 22 | 23 | [mypy-numpy] 24 | ignore_missing_imports = True 25 | 26 | [mypy-PIL] 27 | ignore_missing_imports = True 28 | 29 | [mypy-flatbuffers] 30 | ignore_missing_imports = True 31 | -------------------------------------------------------------------------------- /docs/source/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /examples/imu.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import time 4 | 5 | import pycozmo 6 | 7 | 8 | def on_robot_state(cli, pkt: pycozmo.protocol_encoder.RobotState): 9 | if pkt.pose_angle_rad < -0.4: 10 | state = "LS" 11 | elif pkt.pose_angle_rad > 0.4: 12 | state = "RS" 13 | elif pkt.pose_pitch_rad < -1.0: 14 | state = "F" 15 | elif pkt.pose_pitch_rad > 1.0: 16 | state = "B" 17 | else: 18 | state = "-" 19 | print("{:6.02f}\t{:6.03f}\t{}".format( 20 | pkt.pose_angle_rad, pkt.pose_pitch_rad, state)) 21 | 22 | 23 | with pycozmo.connect(enable_animations=False) as cli: 24 | 25 | cli.add_handler(pycozmo.protocol_encoder.RobotState, on_robot_state) 26 | 27 | while True: 28 | time.sleep(0.1) 29 | -------------------------------------------------------------------------------- /examples/procedural_face.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import time 4 | 5 | from PIL import Image 6 | import numpy as np 7 | 8 | import pycozmo 9 | 10 | 11 | with pycozmo.connect(enable_procedural_face=False) as cli: 12 | 13 | # Raise head. 14 | angle = (pycozmo.robot.MAX_HEAD_ANGLE.radians - pycozmo.robot.MIN_HEAD_ANGLE.radians) / 2.0 15 | cli.set_head_angle(angle) 16 | time.sleep(1) 17 | 18 | # Render a 128x64 procedural face with default parameters. 19 | f = pycozmo.procedural_face.ProceduralFace() 20 | im = f.render() 21 | 22 | # The Cozmo protocol expects a 128x32 image, so take only the even lines. 23 | np_im = np.array(im) 24 | np_im2 = np_im[::2] 25 | im2 = Image.fromarray(np_im2) 26 | 27 | cli.display_image(im2, 5.0) 28 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | 2 | Contributing to PyCozmo 3 | ======================= 4 | 5 | Any contribution to PyCozmo is welcome and appreciated. 6 | 7 | 8 | Bug Reports and Feature Requests 9 | -------------------------------- 10 | 11 | - Bug reports and feature requests should be made using [GitHub issues](https://github.com/zayfod/pycozmo/issues). 12 | 13 | 14 | Pull Requests 15 | ------------- 16 | 17 | - Pull requests should be made against the [dev branch](https://github.com/zayfod/pycozmo/tree/dev). 18 | - All [tests](https://github.com/zayfod/pycozmo/actions) should be passing on a PR, before it can be merged. 19 | - Code should adhere to the [PEP 8 style guide](https://www.python.org/dev/peps/pep-0008/). 20 | - Using docstrings is encouraged. 21 | - Adding tests is encouraged. 22 | -------------------------------------------------------------------------------- /pycozmo/filter.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | ID filtering for logging. 4 | 5 | """ 6 | 7 | from typing import Set 8 | 9 | 10 | __all__ = [ 11 | "Filter", 12 | ] 13 | 14 | 15 | class Filter(object): 16 | 17 | def __init__(self): 18 | self.allowed_ids = set() 19 | self.denied_ids = set() 20 | 21 | def allow_ids(self, ids: Set[int]) -> None: 22 | self.allowed_ids.update(ids) 23 | 24 | def deny_ids(self, ids: Set[int]) -> None: 25 | self.denied_ids.update(ids) 26 | 27 | def filter(self, target_id: int) -> bool: 28 | if target_id is not None: 29 | if self.allowed_ids and target_id not in self.allowed_ids: 30 | return True 31 | if self.denied_ids and target_id in self.denied_ids: 32 | return True 33 | return False 34 | -------------------------------------------------------------------------------- /examples/nvram.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from threading import Event 4 | 5 | import pycozmo 6 | 7 | 8 | e = Event() 9 | 10 | 11 | def on_nv_storage_op_result(cli: pycozmo.client.Client, pkt: pycozmo.protocol_encoder.NvStorageOpResult): 12 | print(pkt.result) 13 | print(pkt.data) 14 | if pkt.result != pycozmo.protocol_encoder.NvResult.NV_MORE: 15 | e.set() 16 | 17 | 18 | with pycozmo.connect(enable_animations=False, log_level="DEBUG") as cli: 19 | 20 | cli.add_handler(pycozmo.protocol_encoder.NvStorageOpResult, on_nv_storage_op_result) 21 | 22 | pkt = pycozmo.protocol_encoder.NvStorageOp( 23 | tag=pycozmo.protocol_encoder.NvEntryTag.NVEntry_CameraCalib, 24 | length=1, 25 | op=pycozmo.protocol_encoder.NvOperation.NVOP_READ) 26 | cli.conn.send(pkt) 27 | 28 | e.wait(timeout=20.0) 29 | -------------------------------------------------------------------------------- /pycozmo/tests/test_decay_graph.py: -------------------------------------------------------------------------------- 1 | 2 | import unittest 3 | 4 | from pycozmo.emotions import DecayGraph, Node 5 | 6 | 7 | class TestDecayGraph(unittest.TestCase): 8 | 9 | def setUp(self): 10 | self.graph = DecayGraph([ 11 | Node(x=0, y=1), 12 | Node(x=10, y=1), 13 | Node(x=60, y=0.6), 14 | Node(x=100, y=0.2), 15 | ]) 16 | 17 | def test_increment_calculation(self): 18 | # negative values 19 | self.assertAlmostEqual(self.graph.get_increment(-1), 1.0) 20 | # value between nodes 21 | self.assertAlmostEqual(self.graph.get_increment(40), 0.76) 22 | # value matches node 23 | self.assertAlmostEqual(self.graph.get_increment(60), 0.6) 24 | # value is higher than last node 25 | self.assertAlmostEqual(self.graph.get_increment(200), -0.8) 26 | -------------------------------------------------------------------------------- /pycozmo/exception.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | Exception declarations. 4 | 5 | """ 6 | 7 | 8 | __all__ = [ 9 | "PyCozmoException", 10 | "PyCozmoConnectionError", 11 | "ConnectionTimeout", 12 | "Timeout", 13 | "NoSpace", 14 | ] 15 | 16 | 17 | class PyCozmoException(Exception): 18 | """ Base class for all PyCozmo exceptions. """ 19 | 20 | 21 | class PyCozmoConnectionError(PyCozmoException): 22 | """ Base class for all PyCozmo connection exceptions. """ 23 | 24 | 25 | class ConnectionTimeout(PyCozmoConnectionError): 26 | """ Connection timeout. """ 27 | 28 | 29 | class Timeout(PyCozmoException): 30 | """ Operation timed out. """ 31 | 32 | 33 | class NoSpace(PyCozmoException): 34 | """ Out of space. """ 35 | 36 | 37 | class InvalidOperation(PyCozmoException): 38 | """ Invalid operation. """ 39 | 40 | 41 | class ResourcesNotFound(PyCozmoException): 42 | """ Cozmo resources not found. """ 43 | -------------------------------------------------------------------------------- /docs/source/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /pycozmo/logger.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | Logging. 4 | 5 | """ 6 | 7 | import logging 8 | 9 | 10 | __all__ = [ 11 | "logger", 12 | "logger_protocol", 13 | "logger_robot", 14 | "logger_reaction", 15 | "logger_behavior", 16 | "logger_animation", 17 | ] 18 | 19 | 20 | # General logger - general PyCozmo log messages. 21 | logger = logging.getLogger("pycozmo.general") 22 | # Protocol logger - log messages related to the Cozmo protocol. 23 | logger_protocol = logging.getLogger("pycozmo.protocol") 24 | # Robot logger - log messages coming from the robot microcontrollers. 25 | logger_robot = logging.getLogger("pycozmo.robot") 26 | # Reaction logger. 27 | logger_reaction = logging.getLogger("pycozmo.reaction") 28 | # Behavior logger. 29 | logger_behavior = logging.getLogger("pycozmo.behavior") 30 | # Animation logger. 31 | logger_animation = logging.getLogger("pycozmo.animation") 32 | 33 | # TODO: See cozmo_resources/config/engine/console_filter_config.json 34 | -------------------------------------------------------------------------------- /pycozmo/CozmoAnim/RecordHeading.py: -------------------------------------------------------------------------------- 1 | # automatically generated by the FlatBuffers compiler, do not modify 2 | 3 | # namespace: CozmoAnim 4 | 5 | import flatbuffers 6 | 7 | class RecordHeading(object): 8 | __slots__ = ['_tab'] 9 | 10 | @classmethod 11 | def GetRootAsRecordHeading(cls, buf, offset): 12 | n = flatbuffers.encode.Get(flatbuffers.packer.uoffset, buf, offset) 13 | x = RecordHeading() 14 | x.Init(buf, n + offset) 15 | return x 16 | 17 | # RecordHeading 18 | def Init(self, buf, pos): 19 | self._tab = flatbuffers.table.Table(buf, pos) 20 | 21 | # RecordHeading 22 | def TriggerTimeMs(self): 23 | o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(4)) 24 | if o != 0: 25 | return self._tab.Get(flatbuffers.number_types.Uint32Flags, o + self._tab.Pos) 26 | return 0 27 | 28 | def RecordHeadingStart(builder): builder.StartObject(1) 29 | def RecordHeadingAddTriggerTimeMs(builder, triggerTimeMs): builder.PrependUint32Slot(0, triggerTimeMs, 0) 30 | def RecordHeadingEnd(builder): return builder.EndObject() 31 | -------------------------------------------------------------------------------- /docs/source/api.rst: -------------------------------------------------------------------------------- 1 | pycozmo package 2 | =============== 3 | 4 | .. autosummary:: 5 | :nosignatures: 6 | :toctree: generated 7 | 8 | pycozmo.audiokinetic.exception 9 | pycozmo.audiokinetic.soundbank 10 | pycozmo.audiokinetic.soundbanksinfo 11 | pycozmo.audiokinetic.wem 12 | pycozmo.expressions.expressions 13 | pycozmo.activity 14 | pycozmo.anim 15 | pycozmo.anim_controller 16 | pycozmo.anim_encoder 17 | pycozmo.audio 18 | pycozmo.behavior 19 | pycozmo.brain 20 | pycozmo.camera 21 | pycozmo.client 22 | pycozmo.conn 23 | pycozmo.emotions 24 | pycozmo.event 25 | pycozmo.exception 26 | pycozmo.filter 27 | pycozmo.frame 28 | pycozmo.image_encoder 29 | pycozmo.lights 30 | pycozmo.logging 31 | pycozmo.object 32 | pycozmo.procedural_face 33 | pycozmo.protocol_ast 34 | pycozmo.protocol_base 35 | pycozmo.protocol_declaration 36 | pycozmo.protocol_encoder 37 | pycozmo.protocol_generator 38 | pycozmo.protocol_utils 39 | pycozmo.robot 40 | pycozmo.robot_debug 41 | pycozmo.run 42 | pycozmo.util 43 | pycozmo.window 44 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2019-2020 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | ========================================================================= 2 | == NOTICE file corresponding to the section 4 d of == 3 | == the Apache License, Version 2.0, == 4 | == in this case for the Anki Cozmo Python SDK code. == 5 | ========================================================================= 6 | Anki Cozmo Python SDK 7 | Copyright 2016-2019 Anki Inc. 8 | This product includes software developed as part of 9 | The Anki Cozmo Python SDK (https://github.com/anki/cozmo-python-sdk). 10 | 11 | 12 | ========================================================================= 13 | == NOTICE file corresponding to the section 4 d of == 14 | == the Apache License, Version 2.0, == 15 | == in this case for the Anki cozmoclad library code. == 16 | ========================================================================= 17 | Anki cozmoclad library 18 | Copyright 2016-2019 Anki Inc. 19 | This product includes software developed as part of 20 | The Anki cozmoclad library (https://pypi.org/project/cozmoclad/). 21 | -------------------------------------------------------------------------------- /tools/pycozmo_app.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | 4 | PyCozmo application. 5 | 6 | """ 7 | 8 | import sys 9 | import time 10 | import argparse 11 | 12 | import pycozmo 13 | 14 | 15 | def parse_args(): 16 | """ Parse command-line arguments. """ 17 | parser = argparse.ArgumentParser(description=__doc__) 18 | parser.add_argument("-v", "--verbose", action="store_true", help="verbose") 19 | args = parser.parse_args() 20 | return args 21 | 22 | 23 | def main(): 24 | # Parse command-line. 25 | args = parse_args() # noqa 26 | 27 | try: 28 | with pycozmo.connect( 29 | log_level="DEBUG" if args.verbose else "INFO", 30 | protocol_log_level="INFO", 31 | robot_log_level="INFO") as cli: 32 | brain = pycozmo.brain.Brain(cli) 33 | brain.start() 34 | while True: 35 | try: 36 | time.sleep(1.0) 37 | except KeyboardInterrupt: 38 | break 39 | brain.stop() 40 | except Exception as e: 41 | print("ERROR: {}".format(e)) 42 | sys.exit(1) 43 | 44 | 45 | if __name__ == '__main__': 46 | main() 47 | -------------------------------------------------------------------------------- /.github/workflows/pythonpackage.yml: -------------------------------------------------------------------------------- 1 | name: Python package 2 | 3 | on: 4 | push: 5 | branches: [master, dev] 6 | pull_request: 7 | branches: [master, dev] 8 | 9 | jobs: 10 | build: 11 | runs-on: ${{ matrix.os }} 12 | 13 | strategy: 14 | matrix: 15 | python-version: [3.6, 3.7, 3.8, 3.9] 16 | os: [ubuntu-latest, windows-latest, macOS-latest] 17 | 18 | steps: 19 | - uses: actions/checkout@v1 20 | - name: Set up Python ${{ matrix.python-version }} 21 | uses: actions/setup-python@v1 22 | with: 23 | python-version: ${{ matrix.python-version }} 24 | - name: Install dependencies 25 | run: | 26 | python -m pip install --upgrade pip 27 | pip install -r requirements.txt 28 | pip install -r requirements-dev.txt 29 | - name: Lint with flake8 30 | run: | 31 | flake8 --version 32 | # stop the build if there are Python syntax errors or undefined names 33 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 34 | flake8 . --count --statistics 35 | - name: Analyze with mypy 36 | run: | 37 | mypy --version 38 | mypy . 39 | continue-on-error: true 40 | - name: Test with pytest 41 | run: | 42 | pytest pycozmo/tests/ 43 | -------------------------------------------------------------------------------- /pycozmo/json_loader.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | JSON reading functions for files containing non-standard comments 4 | 5 | """ 6 | 7 | import json 8 | import os 9 | from typing import Dict, List 10 | 11 | 12 | def load_json_file(filename: str) -> Dict: 13 | with open(filename, 'r') as f: 14 | filtered_json = '' 15 | for line in f.readlines(): 16 | # get all characters before '//' 17 | filtered_json += line.split('//')[0] 18 | return json.loads(filtered_json) 19 | 20 | 21 | def get_json_files(resource_dir: str, base_names: List[str]) -> List[str]: 22 | file_addr = [] 23 | 24 | for name in base_names: 25 | if name[0] == '/': 26 | name = name[1:] 27 | addr = os.path.join(resource_dir, name) 28 | if os.path.isdir(addr): 29 | for root, _, files in os.walk(addr): 30 | for name in files: 31 | if name.endswith('.json'): 32 | file_addr.append(os.path.join(root, name)) 33 | 34 | elif addr.endswith('.json'): 35 | file_addr.append(addr) 36 | 37 | return file_addr 38 | 39 | 40 | def find_file(directory: str, name: str) -> str: 41 | for root, _, files in os.walk(directory): 42 | if name in files: 43 | return os.path.join(root, name) 44 | -------------------------------------------------------------------------------- /pycozmo/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | PyCozmo - a pure-Python Cozmo robot communication library. 4 | 5 | """ 6 | 7 | import sys 8 | 9 | from .logger import * 10 | from .run import * 11 | 12 | from .frame import Frame 13 | from .conn import ROBOT_ADDR 14 | from .client import Client 15 | from .robot import * 16 | from .event import * 17 | 18 | from . import exception 19 | from . import util 20 | from . import window 21 | from . import protocol_base 22 | from . import protocol_declaration 23 | from . import protocol_generator 24 | from . import protocol_encoder 25 | from . import protocol_utils 26 | from . import lights 27 | from . import camera 28 | from . import object 29 | from . import filter 30 | from . import anim 31 | from . import anim_encoder 32 | from . import image_encoder 33 | from . import procedural_face 34 | from . import activity 35 | from . import behavior 36 | from . import emotions 37 | from . import brain 38 | from . import audiokinetic 39 | from . import expressions 40 | 41 | 42 | __version__ = "0.8.0" 43 | 44 | __all__ = [ 45 | "logger", 46 | "logger_protocol", 47 | "logger_robot", 48 | 49 | "Frame", 50 | "ROBOT_ADDR", 51 | "Client", 52 | 53 | "setup_basic_logging", 54 | "connect", 55 | ] 56 | 57 | if sys.version_info < (3, 6, 0): 58 | sys.exit("ERROR: PyCozmo requires Python 3.6.0 or newer.") 59 | del sys 60 | -------------------------------------------------------------------------------- /pycozmo/CozmoAnim/Event.py: -------------------------------------------------------------------------------- 1 | # automatically generated by the FlatBuffers compiler, do not modify 2 | 3 | # namespace: CozmoAnim 4 | 5 | import flatbuffers 6 | 7 | class Event(object): 8 | __slots__ = ['_tab'] 9 | 10 | @classmethod 11 | def GetRootAsEvent(cls, buf, offset): 12 | n = flatbuffers.encode.Get(flatbuffers.packer.uoffset, buf, offset) 13 | x = Event() 14 | x.Init(buf, n + offset) 15 | return x 16 | 17 | # Event 18 | def Init(self, buf, pos): 19 | self._tab = flatbuffers.table.Table(buf, pos) 20 | 21 | # Event 22 | def TriggerTimeMs(self): 23 | o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(4)) 24 | if o != 0: 25 | return self._tab.Get(flatbuffers.number_types.Uint32Flags, o + self._tab.Pos) 26 | return 0 27 | 28 | # Event 29 | def EventId(self): 30 | o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(6)) 31 | if o != 0: 32 | return self._tab.String(o + self._tab.Pos) 33 | return None 34 | 35 | def EventStart(builder): builder.StartObject(2) 36 | def EventAddTriggerTimeMs(builder, triggerTimeMs): builder.PrependUint32Slot(0, triggerTimeMs, 0) 37 | def EventAddEventId(builder, eventId): builder.PrependUOffsetTRelativeSlot(1, flatbuffers.number_types.UOffsetTFlags.py_type(eventId), 0) 38 | def EventEnd(builder): return builder.EndObject() 39 | -------------------------------------------------------------------------------- /examples/cube_lights.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import time 4 | 5 | import pycozmo 6 | 7 | 8 | with pycozmo.connect() as cli: 9 | 10 | print("Waiting for cube...") 11 | cube_factory_id = None 12 | while not cube_factory_id: 13 | available_objects = dict(cli.available_objects) 14 | for factory_id, obj in available_objects.items(): 15 | if obj.object_type == pycozmo.protocol_encoder.ObjectType.Block_LIGHTCUBE1: 16 | cube_factory_id = factory_id 17 | break 18 | print("Cube with S/N 0x{:08x} available.".format(cube_factory_id)) 19 | 20 | print("Connecting to cube...") 21 | pkt = pycozmo.protocol_encoder.ObjectConnect(factory_id=cube_factory_id, connect=True) 22 | cli.conn.send(pkt) 23 | cli.conn.wait_for(pycozmo.protocol_encoder.ObjectConnectionState) 24 | cube_id = list(cli.connected_objects.keys())[0] 25 | print("Cube connected - ID {}.".format(cube_id)) 26 | 27 | lights = [ 28 | pycozmo.lights.red_light, 29 | pycozmo.lights.green_light, 30 | pycozmo.lights.blue_light, 31 | pycozmo.lights.off_light, 32 | ] 33 | for light in lights: 34 | # Select cube 35 | pkt = pycozmo.protocol_encoder.CubeId(object_id=cube_id) 36 | cli.conn.send(pkt) 37 | # Set lights 38 | pkt = pycozmo.protocol_encoder.CubeLights(states=(light, light, light, light)) 39 | cli.conn.send(pkt) 40 | 41 | time.sleep(2) 42 | -------------------------------------------------------------------------------- /pycozmo/CozmoAnim/FaceAnimation.py: -------------------------------------------------------------------------------- 1 | # automatically generated by the FlatBuffers compiler, do not modify 2 | 3 | # namespace: CozmoAnim 4 | 5 | import flatbuffers 6 | 7 | class FaceAnimation(object): 8 | __slots__ = ['_tab'] 9 | 10 | @classmethod 11 | def GetRootAsFaceAnimation(cls, buf, offset): 12 | n = flatbuffers.encode.Get(flatbuffers.packer.uoffset, buf, offset) 13 | x = FaceAnimation() 14 | x.Init(buf, n + offset) 15 | return x 16 | 17 | # FaceAnimation 18 | def Init(self, buf, pos): 19 | self._tab = flatbuffers.table.Table(buf, pos) 20 | 21 | # FaceAnimation 22 | def TriggerTimeMs(self): 23 | o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(4)) 24 | if o != 0: 25 | return self._tab.Get(flatbuffers.number_types.Uint32Flags, o + self._tab.Pos) 26 | return 0 27 | 28 | # FaceAnimation 29 | def AnimName(self): 30 | o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(6)) 31 | if o != 0: 32 | return self._tab.String(o + self._tab.Pos) 33 | return None 34 | 35 | def FaceAnimationStart(builder): builder.StartObject(2) 36 | def FaceAnimationAddTriggerTimeMs(builder, triggerTimeMs): builder.PrependUint32Slot(0, triggerTimeMs, 0) 37 | def FaceAnimationAddAnimName(builder, animName): builder.PrependUOffsetTRelativeSlot(1, flatbuffers.number_types.UOffsetTFlags.py_type(animName), 0) 38 | def FaceAnimationEnd(builder): return builder.EndObject() 39 | -------------------------------------------------------------------------------- /examples/charger_lights.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import time 4 | 5 | import pycozmo 6 | 7 | 8 | with pycozmo.connect() as cli: 9 | 10 | print("Waiting for charger...") 11 | charger_factory_id = None 12 | while not charger_factory_id: 13 | available_objects = dict(cli.available_objects) 14 | for factory_id, obj in available_objects.items(): 15 | if obj.object_type == pycozmo.protocol_encoder.ObjectType.Charger_Basic: 16 | charger_factory_id = factory_id 17 | break 18 | print("Charger with S/N 0x{:08x} available.".format(charger_factory_id)) 19 | 20 | print("Connecting to charger...") 21 | pkt = pycozmo.protocol_encoder.ObjectConnect(factory_id=charger_factory_id, connect=True) 22 | cli.conn.send(pkt) 23 | cli.conn.wait_for(pycozmo.protocol_encoder.ObjectConnectionState) 24 | charger_id = list(cli.connected_objects.keys())[0] 25 | print("Charger connected - ID {}.".format(charger_id)) 26 | 27 | lights = [ 28 | pycozmo.lights.red_light, 29 | pycozmo.lights.green_light, 30 | pycozmo.lights.blue_light, 31 | pycozmo.lights.off_light, 32 | ] 33 | for light in lights: 34 | # Select 35 | pkt = pycozmo.protocol_encoder.CubeId(object_id=charger_id) 36 | cli.conn.send(pkt) 37 | # Set lights 38 | pkt = pycozmo.protocol_encoder.CubeLights(states=(light, light, light, pycozmo.lights.off_light)) 39 | cli.conn.send(pkt) 40 | 41 | time.sleep(2) 42 | -------------------------------------------------------------------------------- /examples/video.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from PIL import Image 4 | import pycozmo 5 | 6 | 7 | # Last image, received from the robot. 8 | last_im = None 9 | 10 | 11 | def on_camera_image(cli, new_im): 12 | """ Handle new images, coming from the robot. """ 13 | global last_im 14 | last_im = new_im 15 | 16 | 17 | with pycozmo.connect(enable_procedural_face=False) as cli: 18 | 19 | # Raise head. 20 | angle = (pycozmo.robot.MAX_HEAD_ANGLE.radians - pycozmo.robot.MIN_HEAD_ANGLE.radians) / 2.0 21 | cli.set_head_angle(angle) 22 | 23 | # Register to receive new camera images. 24 | cli.add_handler(pycozmo.event.EvtNewRawCameraImage, on_camera_image) 25 | 26 | # Enable camera. 27 | cli.enable_camera() 28 | 29 | # Run with 14 FPS. This is the frame rate of the robot camera. 30 | timer = pycozmo.util.FPSTimer(14) 31 | while True: 32 | 33 | if last_im: 34 | 35 | # Get last image. 36 | im = last_im 37 | 38 | # Resize from 320x240 to 68x17. Larger image sometime are too big for the robot receive buffer. 39 | im = im.resize((68, 17)) 40 | # Convert to binary image. 41 | im = im.convert('1') 42 | # Mirror the image. 43 | im = im.transpose(Image.FLIP_LEFT_RIGHT) 44 | # Construct a 128x32 image that the robot can display. 45 | im2 = Image.new("1", (128, 32)) 46 | im2.paste(im, (30, 7)) 47 | # Display the result image. 48 | cli.display_image(im2) 49 | 50 | timer.sleep() 51 | -------------------------------------------------------------------------------- /pycozmo/CozmoAnim/AnimClip.py: -------------------------------------------------------------------------------- 1 | # automatically generated by the FlatBuffers compiler, do not modify 2 | 3 | # namespace: CozmoAnim 4 | 5 | import flatbuffers 6 | 7 | class AnimClip(object): 8 | __slots__ = ['_tab'] 9 | 10 | @classmethod 11 | def GetRootAsAnimClip(cls, buf, offset): 12 | n = flatbuffers.encode.Get(flatbuffers.packer.uoffset, buf, offset) 13 | x = AnimClip() 14 | x.Init(buf, n + offset) 15 | return x 16 | 17 | # AnimClip 18 | def Init(self, buf, pos): 19 | self._tab = flatbuffers.table.Table(buf, pos) 20 | 21 | # AnimClip 22 | def Name(self): 23 | o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(4)) 24 | if o != 0: 25 | return self._tab.String(o + self._tab.Pos) 26 | return None 27 | 28 | # AnimClip 29 | def Keyframes(self): 30 | o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(6)) 31 | if o != 0: 32 | x = self._tab.Indirect(o + self._tab.Pos) 33 | from .Keyframes import Keyframes 34 | obj = Keyframes() 35 | obj.Init(self._tab.Bytes, x) 36 | return obj 37 | return None 38 | 39 | def AnimClipStart(builder): builder.StartObject(2) 40 | def AnimClipAddName(builder, Name): builder.PrependUOffsetTRelativeSlot(0, flatbuffers.number_types.UOffsetTFlags.py_type(Name), 0) 41 | def AnimClipAddKeyframes(builder, keyframes): builder.PrependUOffsetTRelativeSlot(1, flatbuffers.number_types.UOffsetTFlags.py_type(keyframes), 0) 42 | def AnimClipEnd(builder): return builder.EndObject() 43 | -------------------------------------------------------------------------------- /docs/esp8266.md: -------------------------------------------------------------------------------- 1 | 2 | ESP8266 3 | ======= 4 | 5 | The ESP8266 is the main Cozmo controller, responsible for Wi-Fi communication. 6 | 7 | 8 | SPI Flash Memory Map 9 | -------------------- 10 | 11 | The SPI flash size is 2 MB. 12 | 13 | The below memory map has been reconstructed based on a SPI flash memory dump and `NvEntryTag` values. 14 | 15 | ``` 16 | Offset Length Type Description 17 | --------------------------------------------------------------------------------- 18 | 0x00000000 0x00001000 Code Bootloader. 19 | 20 | 0x00001000 0x00001000 Data Unknown. The first 4 bytes are the head serial number. 21 | 0x00002000 0x00001000 Data Unknown. 22 | 23 | 0x00003000 0x0007b800 Code Application image 1. 24 | 0x0007e800 0x00001800 Data Application image 1 signature. See versions.md . 25 | 26 | 0x00080000 0x0005e000 Code Recovery image / factory firmware. 27 | 28 | 0x000de000 0x00000030 Data Birth certificate. 29 | 0x000de030 0x00021fd0 Data Factory data. 30 | 31 | 0x00100000 0x00003000 Data Unknown. 32 | 33 | 0x00103000 0x0007b800 Code Application image 2 34 | 0x0017e800 0x00001800 Data Application image 2 signature. See versions.md . 35 | 36 | 0x00180000 0x00018000 Data Application data. 37 | 0x00198000 0x00028000 Data Empty. 38 | 39 | 0x001c0000 0x0001e000 Data Factory reserved 1. 40 | 0x001de000 0x0001e000 Data Factory reserved 2. Empty? 41 | 42 | 0x001fc000 0x00001000 Data Unknown. 43 | 0x001fd000 0x00001000 Data Wi-Fi configuration 1. 44 | 0x001fe000 0x00001000 Data Wi-Fi configuration 2. 45 | 0x001ff000 0x00001000 Data Unknown. 46 | ``` 47 | -------------------------------------------------------------------------------- /pycozmo/CozmoAnim/AnimClips.py: -------------------------------------------------------------------------------- 1 | # automatically generated by the FlatBuffers compiler, do not modify 2 | 3 | # namespace: CozmoAnim 4 | 5 | import flatbuffers 6 | 7 | class AnimClips(object): 8 | __slots__ = ['_tab'] 9 | 10 | @classmethod 11 | def GetRootAsAnimClips(cls, buf, offset): 12 | n = flatbuffers.encode.Get(flatbuffers.packer.uoffset, buf, offset) 13 | x = AnimClips() 14 | x.Init(buf, n + offset) 15 | return x 16 | 17 | # AnimClips 18 | def Init(self, buf, pos): 19 | self._tab = flatbuffers.table.Table(buf, pos) 20 | 21 | # AnimClips 22 | def Clips(self, j): 23 | o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(4)) 24 | if o != 0: 25 | x = self._tab.Vector(o) 26 | x += flatbuffers.number_types.UOffsetTFlags.py_type(j) * 4 27 | x = self._tab.Indirect(x) 28 | from .AnimClip import AnimClip 29 | obj = AnimClip() 30 | obj.Init(self._tab.Bytes, x) 31 | return obj 32 | return None 33 | 34 | # AnimClips 35 | def ClipsLength(self): 36 | o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(4)) 37 | if o != 0: 38 | return self._tab.VectorLen(o) 39 | return 0 40 | 41 | def AnimClipsStart(builder): builder.StartObject(1) 42 | def AnimClipsAddClips(builder, clips): builder.PrependUOffsetTRelativeSlot(0, flatbuffers.number_types.UOffsetTFlags.py_type(clips), 0) 43 | def AnimClipsStartClipsVector(builder, numElems): return builder.StartVector(4, numElems, 4) 44 | def AnimClipsEnd(builder): return builder.EndObject() 45 | -------------------------------------------------------------------------------- /examples/cube_light_animation.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import time 4 | 5 | import pycozmo 6 | 7 | 8 | with pycozmo.connect() as cli: 9 | 10 | print("Waiting for cube...") 11 | cube_factory_id = None 12 | while not cube_factory_id: 13 | available_objects = dict(cli.available_objects) 14 | for factory_id, obj in available_objects.items(): 15 | if obj.object_type == pycozmo.protocol_encoder.ObjectType.Block_LIGHTCUBE1: 16 | cube_factory_id = factory_id 17 | break 18 | print("Cube with S/N 0x{:08x} available.".format(cube_factory_id)) 19 | 20 | print("Connecting to cube...") 21 | pkt = pycozmo.protocol_encoder.ObjectConnect(factory_id=cube_factory_id, connect=True) 22 | cli.conn.send(pkt) 23 | cli.conn.wait_for(pycozmo.protocol_encoder.ObjectConnectionState) 24 | cube_id = list(cli.connected_objects.keys())[0] 25 | print("Cube connected - ID {}.".format(cube_id)) 26 | 27 | color = pycozmo.lights.Color(int_color=0x00ff00ff) 28 | light = pycozmo.protocol_encoder.LightState( 29 | on_color=color.to_int16(), 30 | off_color=pycozmo.lights.off.to_int16(), 31 | on_frames=5, 32 | off_frames=20, 33 | transition_on_frames=5, 34 | transition_off_frames=10) 35 | 36 | # Select cube 37 | pkt = pycozmo.protocol_encoder.CubeId(object_id=cube_id, rotation_period_frames=40) 38 | cli.conn.send(pkt) 39 | # Set lights 40 | pkt = pycozmo.protocol_encoder.CubeLights(states=( 41 | light, 42 | pycozmo.lights.off_light, 43 | pycozmo.lights.off_light, 44 | pycozmo.lights.off_light)) 45 | cli.conn.send(pkt) 46 | 47 | time.sleep(30.0) 48 | -------------------------------------------------------------------------------- /docs/hardware_versions.md: -------------------------------------------------------------------------------- 1 | 2 | Cozmo Hardware Versions 3 | ======================= 4 | 5 | 6 | Hardware Version 4 7 | ------------------ 8 | 9 | - fall 2016 10 | - does not have a button 11 | - come with platforms with LEDs? 12 | 13 | ``` 14 | 2020-09-23 19:12:56.567 pycozmo.general INFO Firmware version 2381. 15 | 2020-09-23 19:12:56.568 pycozmo.robot INFO hardware.revision: Hardware 1.0 16 | 2020-09-23 19:12:56.598 pycozmo.general INFO Body S/N 0x088xxxxx, HW version 4, color 0. 17 | ``` 18 | 19 | 20 | Hardware Version 5 21 | ------------------ 22 | 23 | - fall 2017 24 | - has an off button (EU Certification) 25 | - observed to have factory firmware v10501 26 | - teardown - https://www.microcontrollertips.com/teardown-anki-cozmo-vector/ 27 | 28 | ``` 29 | 2020-09-26 12:31:32.421 pycozmo.general INFO Firmware version 2381. 30 | 2020-09-26 12:31:32.422 pycozmo.robot INFO hardware.revision: Hardware 1.5 31 | 2020-09-26 12:31:32.453 pycozmo.general INFO Body S/N 0x088xxxxx, HW version 5, color 3. 32 | ``` 33 | 34 | 35 | Hardware Version 6 36 | ------------------ 37 | 38 | - fall 2018 39 | - has an off button (Japan certification) 40 | 41 | 42 | Hardware Version 7 43 | ------------------ 44 | 45 | - fall 2019 46 | - has an on/off button 47 | - observed with development units 48 | - observed to have factory firmware v10700 49 | - observed to report undocumented color "5" 50 | 51 | ``` 52 | 2020-09-24 20:04:35.823 pycozmo.general INFO Firmware version 10700. 53 | 2020-09-24 20:04:35.831 pycozmo.robot INFO hardware.revision: Hardware 1.7 54 | 2020-09-24 20:04:35.856 pycozmo.general INFO Body S/N 0x088xxxxx, HW version 7, color 5. 55 | ``` 56 | -------------------------------------------------------------------------------- /pycozmo/tests/test_robot_debug.py: -------------------------------------------------------------------------------- 1 | 2 | import unittest 3 | import logging 4 | 5 | from pycozmo.robot_debug import get_log_level, get_debug_message 6 | 7 | 8 | class TestLogLevel(unittest.TestCase): 9 | 10 | def test_invalid(self): 11 | level = get_log_level(-1) 12 | self.assertEqual(level, logging.DEBUG) 13 | 14 | def test_debug(self): 15 | level = get_log_level(1) 16 | self.assertEqual(level, logging.DEBUG) 17 | 18 | def test_debug2(self): 19 | level = get_log_level(2) 20 | self.assertEqual(level, logging.DEBUG) 21 | 22 | def test_info(self): 23 | level = get_log_level(3) 24 | self.assertEqual(level, logging.INFO) 25 | 26 | def test_warning(self): 27 | level = get_log_level(4) 28 | self.assertEqual(level, logging.WARNING) 29 | 30 | def test_error(self): 31 | level = get_log_level(5) 32 | self.assertEqual(level, logging.ERROR) 33 | 34 | 35 | class TestDebugMessage(unittest.TestCase): 36 | 37 | def test_no_name_no_format(self): 38 | msg = get_debug_message(-1, -1, []) 39 | self.assertEqual(msg, "") 40 | 41 | def test_no_format(self): 42 | msg = get_debug_message(0, -1, []) 43 | self.assertEqual(msg, "ASSERT") 44 | 45 | def test_no_name(self): 46 | msg = get_debug_message(-1, 0, []) 47 | self.assertEqual(msg, "Invalid format ID") 48 | 49 | def test_name_format(self): 50 | msg = get_debug_message(7, 3, []) 51 | self.assertEqual(msg, "HeadController: Initializing") 52 | 53 | def test_name_format_args(self): 54 | msg = get_debug_message(409, 624, [0, 0x11, 0x22, 0x33, 0x44, 0x55]) 55 | self.assertEqual(msg, "macaddr.soft_ap: 00:11:22:33:44:55") 56 | 57 | def test_name_format_invalid_args(self): 58 | with self.assertRaises(AssertionError): 59 | get_debug_message(409, 624, []) 60 | -------------------------------------------------------------------------------- /pycozmo/tests/test_frame.py: -------------------------------------------------------------------------------- 1 | 2 | import unittest 3 | 4 | from pycozmo.frame import Frame 5 | 6 | 7 | class TestFrame(unittest.TestCase): 8 | 9 | def test_from_bytes_multi(self): 10 | f = Frame.from_bytes( 11 | b'COZ\x03RE\x01\x07\x9d\n\xa0\n\x8f\x00\x04\x01\x00\x8f\x04\x1d\x00\x97\x1a\x00\x15\xb0\xaa\x9c' 12 | b'\xac\xb2@\xa8\xba^\xac\xb2@\x02\xb4\xa2\xa0\xb0\xaa@\xac\xb2`\xb0\xaa\x1b\x04 \x00\x03\x1f\x80' 13 | b'\x1f\x80\t\x00\x00\x00\x00\x00\x1f\x80\x1f\x80\t\x00\x00\x00\x00\x00\x1f\x80\x1f\x80\t\x00\x00' 14 | b'\x00\x00\x00\x00\x04\x16\x00\x11\x1f\x80\x1f\x80\t\x00\x00\x00\x00\x00\x1f\x80\x1f\x80\t\x00' 15 | b'\x00\x00\x00\x00\x00') 16 | self.assertEqual(f.type.value, 7) 17 | self.assertEqual(f.first_seq, 2716) 18 | self.assertEqual(f.seq, 2719) 19 | self.assertEqual(f.ack, 142) 20 | self.assertEqual(len(f.pkts), 4) 21 | 22 | def test_encode_decode(self): 23 | expected = \ 24 | b'COZ\x03RE\x01\x07\x9d\n\xa0\n\x8f\x00\x04\x01\x00\x8f\x04\x1d\x00\x97\x1a\x00\x15\xb0\xaa\x9c' \ 25 | b'\xac\xb2@\xa8\xba^\xac\xb2@\x02\xb4\xa2\xa0\xb0\xaa@\xac\xb2`\xb0\xaa\x1b\x04 \x00\x03\x1f\x80' \ 26 | b'\x1f\x80\t\x00\x00\x00\x00\x00\x1f\x80\x1f\x80\t\x00\x00\x00\x00\x00\x1f\x80\x1f\x80\t\x00\x00' \ 27 | b'\x00\x00\x00\x00\x04\x16\x00\x11\x1f\x80\x1f\x80\t\x00\x00\x00\x00\x00\x1f\x80\x1f\x80\t\x00' \ 28 | b'\x00\x00\x00\x00\x00' 29 | f = Frame.from_bytes(expected) 30 | actual = f.to_bytes() 31 | self.assertEqual(expected, actual) 32 | 33 | def test_ignore_decode_failures(self): 34 | # v2214 AnimationState packet with no client_drop_count field is ignored. 35 | f = Frame.from_bytes( 36 | b'COZ\x03RE\x01\t\x00\x00\x00\x00\x13\x00\x0b\x11\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00' 37 | b'\x00\x00\x00\x00\x00\x00\x00\x05\x0f\x00\xf1y\x8bJO$\x00\x00\x00\x06\x00\x00\x00\xff\x00') 38 | assert len(f.pkts) == 1 39 | -------------------------------------------------------------------------------- /examples/display_lines.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import time 4 | import random 5 | import itertools 6 | 7 | from PIL import Image, ImageDraw 8 | 9 | import pycozmo 10 | 11 | 12 | WIDTH = 128 13 | HEIGHT = 32 14 | MAX_SPEED = 2 15 | NUM_DOTS = 3 16 | DOT_SIZE = 1 17 | LINE_WIDTH = 1 18 | 19 | 20 | class Dot(object): 21 | 22 | def __init__(self, x: int, y: int, vx: int, vy: int): 23 | self.x = x 24 | self.y = y 25 | self.vx = vx 26 | self.vy = vy 27 | 28 | 29 | with pycozmo.connect(enable_procedural_face=False) as cli: 30 | 31 | # Raise head. 32 | angle = (pycozmo.robot.MAX_HEAD_ANGLE.radians - pycozmo.robot.MIN_HEAD_ANGLE.radians) / 2.0 33 | cli.set_head_angle(angle) 34 | time.sleep(1) 35 | 36 | # Generate random dots. 37 | dots = [] 38 | for i in range(NUM_DOTS): 39 | x = random.randint(0, WIDTH) 40 | y = random.randint(0, HEIGHT) 41 | vx = random.randint(-MAX_SPEED, MAX_SPEED) 42 | vy = random.randint(-MAX_SPEED, MAX_SPEED) 43 | dot = Dot(x, y, vx, vy) 44 | dots.append(dot) 45 | 46 | timer = pycozmo.util.FPSTimer(pycozmo.robot.FRAME_RATE) 47 | while True: 48 | 49 | # Create a blank image. 50 | im = Image.new("1", (128, 32), color=0) 51 | 52 | # Draw lines. 53 | draw = ImageDraw.Draw(im) 54 | for a, b in itertools.combinations(dots, 2): 55 | draw.line((a.x, a.y, b.x, b.y), width=LINE_WIDTH, fill=1) 56 | 57 | # Move dots. 58 | for dot in dots: 59 | dot.x += dot.vx 60 | dot.y += dot.vy 61 | if dot.x <= DOT_SIZE: 62 | dot.x = DOT_SIZE 63 | dot.vx = abs(dot.vx) 64 | elif dot.x >= WIDTH - DOT_SIZE: 65 | dot.x = WIDTH - DOT_SIZE 66 | dot.vx = -abs(dot.vx) 67 | if dot.y <= DOT_SIZE: 68 | dot.y = DOT_SIZE 69 | dot.vy = abs(dot.vy) 70 | elif dot.y >= HEIGHT - DOT_SIZE: 71 | dot.y = HEIGHT - DOT_SIZE 72 | dot.vy = -abs(dot.vy) 73 | 74 | cli.display_image(im) 75 | 76 | # Run with 30 FPS. 77 | timer.sleep() 78 | -------------------------------------------------------------------------------- /pycozmo/CozmoAnim/BodyMotion.py: -------------------------------------------------------------------------------- 1 | # automatically generated by the FlatBuffers compiler, do not modify 2 | 3 | # namespace: CozmoAnim 4 | 5 | import flatbuffers 6 | 7 | class BodyMotion(object): 8 | __slots__ = ['_tab'] 9 | 10 | @classmethod 11 | def GetRootAsBodyMotion(cls, buf, offset): 12 | n = flatbuffers.encode.Get(flatbuffers.packer.uoffset, buf, offset) 13 | x = BodyMotion() 14 | x.Init(buf, n + offset) 15 | return x 16 | 17 | # BodyMotion 18 | def Init(self, buf, pos): 19 | self._tab = flatbuffers.table.Table(buf, pos) 20 | 21 | # BodyMotion 22 | def TriggerTimeMs(self): 23 | o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(4)) 24 | if o != 0: 25 | return self._tab.Get(flatbuffers.number_types.Uint32Flags, o + self._tab.Pos) 26 | return 0 27 | 28 | # BodyMotion 29 | def DurationTimeMs(self): 30 | o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(6)) 31 | if o != 0: 32 | return self._tab.Get(flatbuffers.number_types.Uint32Flags, o + self._tab.Pos) 33 | return 0 34 | 35 | # BodyMotion 36 | def RadiusMm(self): 37 | o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(8)) 38 | if o != 0: 39 | return self._tab.String(o + self._tab.Pos) 40 | return None 41 | 42 | # BodyMotion 43 | def Speed(self): 44 | o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(10)) 45 | if o != 0: 46 | return self._tab.Get(flatbuffers.number_types.Int16Flags, o + self._tab.Pos) 47 | return 0 48 | 49 | def BodyMotionStart(builder): builder.StartObject(4) 50 | def BodyMotionAddTriggerTimeMs(builder, triggerTimeMs): builder.PrependUint32Slot(0, triggerTimeMs, 0) 51 | def BodyMotionAddDurationTimeMs(builder, durationTimeMs): builder.PrependUint32Slot(1, durationTimeMs, 0) 52 | def BodyMotionAddRadiusMm(builder, radiusMm): builder.PrependUOffsetTRelativeSlot(2, flatbuffers.number_types.UOffsetTFlags.py_type(radiusMm), 0) 53 | def BodyMotionAddSpeed(builder, speed): builder.PrependInt16Slot(3, speed, 0) 54 | def BodyMotionEnd(builder): return builder.EndObject() 55 | -------------------------------------------------------------------------------- /examples/path.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from threading import Event 4 | 5 | import pycozmo 6 | 7 | 8 | SPEED_MMPS = 100.0 9 | ACCEL_MMPS2 = 20.0 10 | DECEL_MMPS2 = 20.0 11 | 12 | e = Event() 13 | 14 | 15 | def on_path_following_event(cli, pkt: pycozmo.protocol_encoder.PathFollowingEvent): 16 | print(pkt.event_type) 17 | if pkt.event_type != pycozmo.protocol_encoder.PathEventType.PATH_STARTED: 18 | e.set() 19 | 20 | 21 | def on_robot_pathing_change(cli, state: bool): 22 | if state: 23 | print("Started pathing.") 24 | else: 25 | print("Stopped pathing.") 26 | 27 | 28 | with pycozmo.connect() as cli: 29 | 30 | cli.add_handler(pycozmo.protocol_encoder.PathFollowingEvent, on_path_following_event) 31 | cli.add_handler(pycozmo.event.EvtRobotPathingChange, on_robot_pathing_change) 32 | 33 | pkt = pycozmo.protocol_encoder.AppendPathSegLine( 34 | from_x=0.0, from_y=0.0, 35 | to_x=150.0, to_y=0.0, 36 | speed_mmps=SPEED_MMPS, accel_mmps2=ACCEL_MMPS2, decel_mmps2=DECEL_MMPS2) 37 | cli.conn.send(pkt) 38 | pkt = pycozmo.protocol_encoder.AppendPathSegLine( 39 | from_x=150.0, from_y=0.0, 40 | to_x=150.0, to_y=150.0, 41 | speed_mmps=SPEED_MMPS, accel_mmps2=ACCEL_MMPS2, decel_mmps2=DECEL_MMPS2) 42 | cli.conn.send(pkt) 43 | pkt = pycozmo.protocol_encoder.AppendPathSegLine( 44 | from_x=150.0, from_y=150.0, 45 | to_x=0.0, to_y=150.0, 46 | speed_mmps=SPEED_MMPS, accel_mmps2=ACCEL_MMPS2, decel_mmps2=DECEL_MMPS2) 47 | cli.conn.send(pkt) 48 | pkt = pycozmo.protocol_encoder.AppendPathSegLine( 49 | from_x=0.0, from_y=150.0, 50 | to_x=0.0, to_y=0.0, 51 | speed_mmps=SPEED_MMPS, accel_mmps2=ACCEL_MMPS2, decel_mmps2=DECEL_MMPS2) 52 | cli.conn.send(pkt) 53 | pkt = pycozmo.protocol_encoder.AppendPathSegPointTurn( 54 | x=0.0, y=0.0, 55 | angle_rad=pycozmo.util.Angle(degrees=0.0).radians, 56 | angle_tolerance_rad=0.01, 57 | speed_mmps=SPEED_MMPS, accel_mmps2=ACCEL_MMPS2, decel_mmps2=DECEL_MMPS2) 58 | cli.conn.send(pkt) 59 | 60 | pkt = pycozmo.protocol_encoder.ExecutePath(event_id=1) 61 | cli.conn.send(pkt) 62 | 63 | e.wait(timeout=30.0) 64 | -------------------------------------------------------------------------------- /pycozmo/CozmoAnim/HeadAngle.py: -------------------------------------------------------------------------------- 1 | # automatically generated by the FlatBuffers compiler, do not modify 2 | 3 | # namespace: CozmoAnim 4 | 5 | import flatbuffers 6 | 7 | class HeadAngle(object): 8 | __slots__ = ['_tab'] 9 | 10 | @classmethod 11 | def GetRootAsHeadAngle(cls, buf, offset): 12 | n = flatbuffers.encode.Get(flatbuffers.packer.uoffset, buf, offset) 13 | x = HeadAngle() 14 | x.Init(buf, n + offset) 15 | return x 16 | 17 | # HeadAngle 18 | def Init(self, buf, pos): 19 | self._tab = flatbuffers.table.Table(buf, pos) 20 | 21 | # HeadAngle 22 | def TriggerTimeMs(self): 23 | o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(4)) 24 | if o != 0: 25 | return self._tab.Get(flatbuffers.number_types.Uint32Flags, o + self._tab.Pos) 26 | return 0 27 | 28 | # HeadAngle 29 | def DurationTimeMs(self): 30 | o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(6)) 31 | if o != 0: 32 | return self._tab.Get(flatbuffers.number_types.Uint32Flags, o + self._tab.Pos) 33 | return 0 34 | 35 | # HeadAngle 36 | def AngleDeg(self): 37 | o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(8)) 38 | if o != 0: 39 | return self._tab.Get(flatbuffers.number_types.Int8Flags, o + self._tab.Pos) 40 | return 0 41 | 42 | # HeadAngle 43 | def AngleVariabilityDeg(self): 44 | o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(10)) 45 | if o != 0: 46 | return self._tab.Get(flatbuffers.number_types.Uint8Flags, o + self._tab.Pos) 47 | return 0 48 | 49 | def HeadAngleStart(builder): builder.StartObject(4) 50 | def HeadAngleAddTriggerTimeMs(builder, triggerTimeMs): builder.PrependUint32Slot(0, triggerTimeMs, 0) 51 | def HeadAngleAddDurationTimeMs(builder, durationTimeMs): builder.PrependUint32Slot(1, durationTimeMs, 0) 52 | def HeadAngleAddAngleDeg(builder, angleDeg): builder.PrependInt8Slot(2, angleDeg, 0) 53 | def HeadAngleAddAngleVariabilityDeg(builder, angleVariabilityDeg): builder.PrependUint8Slot(3, angleVariabilityDeg, 0) 54 | def HeadAngleEnd(builder): return builder.EndObject() 55 | -------------------------------------------------------------------------------- /pycozmo/CozmoAnim/LiftHeight.py: -------------------------------------------------------------------------------- 1 | # automatically generated by the FlatBuffers compiler, do not modify 2 | 3 | # namespace: CozmoAnim 4 | 5 | import flatbuffers 6 | 7 | class LiftHeight(object): 8 | __slots__ = ['_tab'] 9 | 10 | @classmethod 11 | def GetRootAsLiftHeight(cls, buf, offset): 12 | n = flatbuffers.encode.Get(flatbuffers.packer.uoffset, buf, offset) 13 | x = LiftHeight() 14 | x.Init(buf, n + offset) 15 | return x 16 | 17 | # LiftHeight 18 | def Init(self, buf, pos): 19 | self._tab = flatbuffers.table.Table(buf, pos) 20 | 21 | # LiftHeight 22 | def TriggerTimeMs(self): 23 | o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(4)) 24 | if o != 0: 25 | return self._tab.Get(flatbuffers.number_types.Uint32Flags, o + self._tab.Pos) 26 | return 0 27 | 28 | # LiftHeight 29 | def DurationTimeMs(self): 30 | o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(6)) 31 | if o != 0: 32 | return self._tab.Get(flatbuffers.number_types.Uint32Flags, o + self._tab.Pos) 33 | return 0 34 | 35 | # LiftHeight 36 | def HeightMm(self): 37 | o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(8)) 38 | if o != 0: 39 | return self._tab.Get(flatbuffers.number_types.Uint8Flags, o + self._tab.Pos) 40 | return 0 41 | 42 | # LiftHeight 43 | def HeightVariabilityMm(self): 44 | o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(10)) 45 | if o != 0: 46 | return self._tab.Get(flatbuffers.number_types.Uint8Flags, o + self._tab.Pos) 47 | return 0 48 | 49 | def LiftHeightStart(builder): builder.StartObject(4) 50 | def LiftHeightAddTriggerTimeMs(builder, triggerTimeMs): builder.PrependUint32Slot(0, triggerTimeMs, 0) 51 | def LiftHeightAddDurationTimeMs(builder, durationTimeMs): builder.PrependUint32Slot(1, durationTimeMs, 0) 52 | def LiftHeightAddHeightMm(builder, heightMm): builder.PrependUint8Slot(2, heightMm, 0) 53 | def LiftHeightAddHeightVariabilityMm(builder, heightVariabilityMm): builder.PrependUint8Slot(3, heightVariabilityMm, 0) 54 | def LiftHeightEnd(builder): return builder.EndObject() 55 | -------------------------------------------------------------------------------- /pycozmo/tests/test_conn.py: -------------------------------------------------------------------------------- 1 | 2 | import unittest 3 | from threading import Event 4 | 5 | import pycozmo 6 | 7 | 8 | class TestConnection(unittest.TestCase): 9 | 10 | @classmethod 11 | def setUpClass(cls): 12 | pycozmo.setup_basic_logging(log_level="DEBUG", protocol_log_level="DEBUG") 13 | 14 | def setUp(self): 15 | self.s_conn_e = Event() 16 | self.c_conn_e = Event() 17 | self.s_e = Event() 18 | self.c_e = Event() 19 | self.s = pycozmo.conn.Connection(server=True) 20 | self.s.add_handler(pycozmo.protocol_encoder.Connect, lambda cli, pkt: self.s_conn_e.set()) 21 | self.c = pycozmo.conn.Connection(("127.0.0.1", 5551)) 22 | self.c.add_handler(pycozmo.protocol_encoder.Connect, lambda cli, pkt: self.c_conn_e.set()) 23 | 24 | def start(self): 25 | self.s.start() 26 | self.c.start() 27 | 28 | def stop(self): 29 | self.c.stop() 30 | self.s.stop() 31 | 32 | def connect(self): 33 | self.c.connect() 34 | self.assertTrue(self.s_conn_e.wait(2.0)) 35 | self.assertTrue(self.c_conn_e.wait(2.0)) 36 | 37 | def disconnect(self): 38 | self.c.disconnect() 39 | 40 | def test_connect(self): 41 | self.start() 42 | self.connect() 43 | self.disconnect() 44 | self.stop() 45 | 46 | def test_ping(self): 47 | self.s.add_handler(pycozmo.protocol_encoder.Ping, lambda cli, pkt: self.s_e.set()) 48 | self.c.add_handler(pycozmo.protocol_encoder.Ping, lambda cli, pkt: self.c_e.set()) 49 | self.start() 50 | self.connect() 51 | self.assertTrue(self.c_e.wait(20000.0)) 52 | self.assertTrue(self.s_e.wait(2.0)) 53 | self.stop() 54 | 55 | @unittest.skip("Intermittently failing.") 56 | def test_send_30(self): 57 | COUNT = 30 58 | counts = [] 59 | self.s.add_handler(pycozmo.protocol_encoder.SetRobotVolume, 60 | lambda cli, pkt: (counts.append(pkt.level), (pkt.level < COUNT - 2) or self.s_e.set())) 61 | self.start() 62 | self.connect() 63 | for i in range(COUNT): 64 | self.c.send(pycozmo.protocol_encoder.SetRobotVolume(i)) 65 | self.assertTrue(self.s_e.wait(5.0)) 66 | self.assertEqual(counts, list(range(COUNT))) 67 | self.stop() 68 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | PyCozmo setup script. 4 | 5 | """ 6 | 7 | import os 8 | import sys 9 | import re 10 | import setuptools 11 | from distutils.version import LooseVersion 12 | 13 | 14 | def get_package_variable(key): 15 | fspec = os.path.join("pycozmo", "__init__.py") 16 | with open(fspec) as f: 17 | for line in f: 18 | m = re.match(r"(\S+)\s*=\s*[\"']?(.+?)[\"']?\s*$", line) 19 | if m and key == m.group(1): 20 | return m.group(2) 21 | return None 22 | 23 | 24 | def get_readme(): 25 | with open("README.md") as f: 26 | readme = f.read() 27 | return readme 28 | 29 | 30 | # Check for setuptools version as long_description_content_type is not supported in older versions. 31 | if LooseVersion(setuptools.__version__) < LooseVersion("38.6.0"): 32 | sys.exit("ERROR: setuptools 38.6.0 or newer required.") 33 | 34 | setuptools.setup( 35 | name="pycozmo", 36 | packages=setuptools.find_packages(), 37 | version=get_package_variable("__version__"), 38 | license="MIT", 39 | description="A pure-Python communication library, alternative SDK, and application for the Cozmo robot.", 40 | long_description=get_readme(), 41 | long_description_content_type="text/markdown", 42 | author="Kaloyan Tenchov", 43 | author_email="zayfod@gmail.com", 44 | url="https://github.com/zayfod/pycozmo/", 45 | python_requires=">=3.6.0", 46 | install_requires=["dpkt", "numpy", "Pillow>=6.0.0", "flatbuffers"], 47 | keywords=["ddl", "anki", "cozmo", "robot", "robotics"], 48 | classifiers=[ 49 | "Development Status :: 4 - Beta", 50 | "License :: OSI Approved :: MIT License", 51 | "Programming Language :: Python", 52 | "Programming Language :: Python :: 3.6", 53 | "Programming Language :: Python :: 3.7", 54 | "Programming Language :: Python :: 3.8", 55 | "Programming Language :: Python :: 3.9", 56 | "Topic :: Software Development :: Libraries", 57 | "Topic :: Software Development :: Libraries :: Python Modules", 58 | "Intended Audience :: Developers", 59 | "Intended Audience :: Education", 60 | ], 61 | scripts=[ 62 | "tools/pycozmo_dump.py", 63 | "tools/pycozmo_replay.py", 64 | "tools/pycozmo_update.py", 65 | "tools/pycozmo_resources.py", 66 | "tools/pycozmo_app.py", 67 | ], 68 | ) 69 | -------------------------------------------------------------------------------- /pycozmo/CozmoAnim/cozmo_anim.fbs: -------------------------------------------------------------------------------- 1 | 2 | namespace CozmoAnim; 3 | 4 | table LiftHeight { 5 | triggerTime_ms:uint; 6 | durationTime_ms:uint; 7 | height_mm:ubyte; 8 | heightVariability_mm:ubyte = 0; 9 | } 10 | 11 | table ProceduralFace { 12 | triggerTime_ms:uint; 13 | faceAngle:float = 0.0; 14 | faceCenterX:float = 0.0; 15 | faceCenterY:float = 0.0; 16 | faceScaleX:float = 1.0; 17 | faceScaleY:float = 1.0; 18 | leftEye:[float]; 19 | rightEye:[float]; 20 | } 21 | 22 | table HeadAngle { 23 | triggerTime_ms:uint; 24 | durationTime_ms:uint; 25 | angle_deg:byte; 26 | angleVariability_deg:ubyte = 0; 27 | } 28 | 29 | table RobotAudio { 30 | triggerTime_ms:uint; 31 | audioEventId:[long] (required); 32 | volume:float = 1.0; 33 | probability:[float]; 34 | hasAlts:bool = true; 35 | } 36 | 37 | table BackpackLights { 38 | triggerTime_ms:uint; 39 | durationTime_ms:uint; 40 | Left:[float]; 41 | Right:[float]; 42 | Front:[float]; 43 | Middle:[float]; 44 | Back:[float]; 45 | } 46 | 47 | table FaceAnimation { 48 | triggerTime_ms:uint; 49 | animName:string (required); 50 | } 51 | 52 | table Event { 53 | triggerTime_ms:uint; 54 | event_id:string (required); 55 | } 56 | 57 | table BodyMotion { 58 | triggerTime_ms:uint; 59 | durationTime_ms:uint; 60 | radius_mm:string (required); 61 | speed:short; 62 | } 63 | 64 | table RecordHeading { 65 | triggerTime_ms:uint; 66 | } 67 | 68 | table TurnToRecordedHeading { 69 | triggerTime_ms:uint; 70 | durationTime_ms:uint; 71 | offset_deg:short = 0; 72 | speed_degPerSec:short; 73 | accel_degPerSec2:short = 1000; 74 | decel_degPerSec2:short = 1000; 75 | tolerance_deg:ushort = 2; 76 | numHalfRevs:ushort = 0; 77 | useShortestDir:bool = false; 78 | } 79 | 80 | table Keyframes { 81 | LiftHeightKeyFrame:[LiftHeight]; 82 | ProceduralFaceKeyFrame:[ProceduralFace]; 83 | HeadAngleKeyFrame:[HeadAngle]; 84 | RobotAudioKeyFrame:[RobotAudio]; 85 | BackpackLightsKeyFrame:[BackpackLights]; 86 | FaceAnimationKeyFrame:[FaceAnimation]; 87 | EventKeyFrame:[Event]; 88 | BodyMotionKeyFrame:[BodyMotion]; 89 | RecordHeadingKeyFrame:[RecordHeading]; 90 | TurnToRecordedHeadingKeyFrame:[TurnToRecordedHeading]; 91 | } 92 | 93 | table AnimClip { 94 | Name:string; 95 | keyframes:Keyframes; 96 | } 97 | 98 | table AnimClips { 99 | clips:[AnimClip]; 100 | } 101 | 102 | root_type AnimClips; 103 | 104 | -------------------------------------------------------------------------------- /pycozmo/audio.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | Cozmo audio encoding. 4 | 5 | References: 6 | - https://en.wikipedia.org/wiki/%CE%9C-law_algorithm 7 | - http://dystopiancode.blogspot.com/2012/02/pcm-law-and-u-law-companding-algorithms.html 8 | 9 | """ 10 | 11 | from typing import List 12 | import struct 13 | import wave 14 | import time 15 | 16 | from . import logger 17 | from . import protocol_encoder 18 | 19 | 20 | __all__ = [ 21 | "load_wav", 22 | ] 23 | 24 | 25 | MULAW_MAX = 0x7FFF 26 | MULAW_BIAS = 132 27 | 28 | 29 | def load_wav(filename: str) -> List[protocol_encoder.OutputAudio]: 30 | """ Load a WAVE file into a list of OutputAudio packets. """ 31 | 32 | start_time = time.perf_counter() 33 | 34 | with wave.open(filename, "r") as w: 35 | sampwidth = w.getsampwidth() 36 | framerate = w.getframerate() 37 | if sampwidth != 2 or (framerate != 22050 and framerate != 48000): 38 | raise ValueError('Invalid audio format, only 16 bit samples are supported, ' + 39 | 'with 22050Hz or 48000Hz frame rates.') 40 | 41 | ratediv = 2 if framerate == 48000 else 1 42 | channels = w.getnchannels() 43 | pkts = [] 44 | 45 | while True: 46 | frame_in = w.readframes(744 * ratediv) 47 | if not frame_in: 48 | break 49 | frame_out = bytes_to_cozmo(frame_in, ratediv, channels) 50 | pkt = protocol_encoder.OutputAudio(samples=frame_out) 51 | pkts.append(pkt) 52 | 53 | logger.debug("Loaded WAVE file in {:.02f} s.".format(time.perf_counter() - start_time)) 54 | 55 | return pkts 56 | 57 | 58 | def bytes_to_cozmo(byte_string: bytes, rate_correction: int, channels: int) -> bytearray: 59 | """ Convert a 744 sample, 16-bit audio frame into a U-law encoded frame. """ 60 | out = bytearray(744) 61 | n = channels * rate_correction 62 | bs = struct.unpack('{}h'.format(int(len(byte_string) / 2)), byte_string)[0::n] 63 | for i, s in enumerate(bs): 64 | out[i] = u_law_encoding(s) 65 | return out 66 | 67 | 68 | def u_law_encoding(sample: int) -> int: 69 | """ U-law encode a 16-bit PCM sample. """ 70 | mask = 0x4000 71 | position = 14 72 | sign = 0 73 | if sample < 0: 74 | sample = -sample 75 | sign = 0x80 76 | sample += MULAW_BIAS 77 | if sample > MULAW_MAX: 78 | sample = MULAW_MAX 79 | 80 | while (sample & mask) != mask and position >= 7: 81 | mask >>= 1 82 | position -= 1 83 | 84 | lsb = (sample >> (position - 4)) & 0x0f 85 | return -(~(sign | ((position - 7) << 4) | lsb)) 86 | -------------------------------------------------------------------------------- /examples/procedural_face_expressions.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import time 4 | 5 | from PIL import Image 6 | import numpy as np 7 | 8 | import pycozmo 9 | 10 | 11 | with pycozmo.connect(enable_procedural_face=False) as cli: 12 | 13 | # Raise head. 14 | angle = (pycozmo.robot.MAX_HEAD_ANGLE.radians - pycozmo.robot.MIN_HEAD_ANGLE.radians) / 2.0 15 | cli.set_head_angle(angle) 16 | time.sleep(1) 17 | 18 | # List of face expressions. 19 | expressions = [ 20 | pycozmo.expressions.Anger(), 21 | pycozmo.expressions.Sadness(), 22 | pycozmo.expressions.Happiness(), 23 | pycozmo.expressions.Surprise(), 24 | pycozmo.expressions.Disgust(), 25 | pycozmo.expressions.Fear(), 26 | pycozmo.expressions.Pleading(), 27 | pycozmo.expressions.Vulnerability(), 28 | pycozmo.expressions.Despair(), 29 | pycozmo.expressions.Guilt(), 30 | pycozmo.expressions.Disappointment(), 31 | pycozmo.expressions.Embarrassment(), 32 | pycozmo.expressions.Horror(), 33 | pycozmo.expressions.Skepticism(), 34 | pycozmo.expressions.Annoyance(), 35 | pycozmo.expressions.Fury(), 36 | pycozmo.expressions.Suspicion(), 37 | pycozmo.expressions.Rejection(), 38 | pycozmo.expressions.Boredom(), 39 | pycozmo.expressions.Tiredness(), 40 | pycozmo.expressions.Asleep(), 41 | pycozmo.expressions.Confusion(), 42 | pycozmo.expressions.Amazement(), 43 | pycozmo.expressions.Excitement(), 44 | ] 45 | 46 | # Base face expression. 47 | base_face = pycozmo.expressions.Neutral() 48 | 49 | rate = pycozmo.robot.FRAME_RATE 50 | timer = pycozmo.util.FPSTimer(rate) 51 | for expression in expressions: 52 | 53 | # Transition from base face to expression and back. 54 | for from_face, to_face in ((base_face, expression), (expression, base_face)): 55 | 56 | if to_face != base_face: 57 | print(to_face.__class__.__name__) 58 | 59 | # Generate transition frames. 60 | face_generator = pycozmo.procedural_face.interpolate(from_face, to_face, rate // 3) 61 | for face in face_generator: 62 | 63 | # Render face image. 64 | im = face.render() 65 | 66 | # The Cozmo protocol expects a 128x32 image, so take only the even lines. 67 | np_im = np.array(im) 68 | np_im2 = np_im[::2] 69 | im2 = Image.fromarray(np_im2) 70 | 71 | # Display face image. 72 | cli.display_image(im2) 73 | 74 | # Maintain frame rate. 75 | timer.sleep() 76 | 77 | # Pause for 1s. 78 | for i in range(rate): 79 | timer.sleep() 80 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | 13 | import os 14 | import glob 15 | from shutil import copyfile 16 | import sys 17 | sys.path.insert(0, os.path.abspath('../..')) # noqa 18 | 19 | # noinspection PyPep8 20 | import pycozmo # noqa 21 | 22 | 23 | # -- Project information ----------------------------------------------------- 24 | 25 | project = 'PyCozmo' 26 | # noinspection PyShadowingBuiltins 27 | copyright = '2019-2020, Kaloyan Tenchov' 28 | author = 'Kaloyan Tenchov' 29 | 30 | # The full version, including alpha/beta/rc tags 31 | release = pycozmo.__version__ 32 | 33 | 34 | # -- General configuration --------------------------------------------------- 35 | 36 | # Add any Sphinx extension module names here, as strings. They can be 37 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 38 | # ones. 39 | extensions = [ 40 | 'sphinx.ext.autodoc', 41 | 'sphinx.ext.autosummary', 42 | 'sphinx.ext.coverage', 43 | 'sphinx.ext.intersphinx', 44 | 'recommonmark', 45 | ] 46 | 47 | # Add any paths that contain templates here, relative to this directory. 48 | templates_path = [] 49 | 50 | # List of patterns, relative to source directory, that match files and 51 | # directories to ignore when looking for source files. 52 | # This pattern also affects html_static_path and html_extra_path. 53 | exclude_patterns = ['**tests**'] 54 | 55 | master_doc = 'index' 56 | 57 | 58 | # -- Options for HTML output ------------------------------------------------- 59 | 60 | # Add any paths that contain custom static files (such as style sheets) here, 61 | # relative to this directory. They are copied after the builtin static files, 62 | # so a file named "default.css" will overwrite the builtin "default.css". 63 | html_static_path = [] 64 | 65 | 66 | autosummary_generate = True 67 | autodoc_default_flags = [ 68 | 'members', 69 | 'undoc-members', 70 | 'show-inheritance', 71 | 'inherited-members', 72 | ] 73 | intersphinx_mapping = { 74 | 'python': ('https://docs.python.org/3.6', None), 75 | 'numpy': ('https://docs.scipy.org/doc/numpy/', None), 76 | 'PIL': ('https://pillow.readthedocs.io/en/latest/', None), 77 | } 78 | 79 | 80 | # Create external/ subdirectory and copy markdown files from docs/ 81 | cur_dir = os.path.dirname(__file__) 82 | external_dir = os.path.join(cur_dir, 'external') 83 | if not os.path.exists(external_dir): 84 | os.mkdir(external_dir) 85 | doc_spec = os.path.join(cur_dir, '..', '*.md') 86 | for src in glob.glob(doc_spec): 87 | dst = os.path.join(external_dir, os.path.basename(src)) 88 | copyfile(src, dst) 89 | -------------------------------------------------------------------------------- /examples/events.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import time 4 | 5 | import pycozmo 6 | 7 | 8 | def on_robot_state(cli, pkt: pycozmo.protocol_encoder.RobotState): 9 | print("Battery level: {:.01f} V".format(pkt.battery_voltage)) 10 | 11 | 12 | def on_robot_poked(cli, pkt: pycozmo.protocol_encoder.RobotPoked): 13 | print("Robot poked.") 14 | 15 | 16 | def on_robot_falling_started(cli, pkt: pycozmo.protocol_encoder.FallingStarted): 17 | print("Started falling.") 18 | 19 | 20 | def on_robot_falling_stopped(cli, pkt: pycozmo.protocol_encoder.FallingStopped): 21 | print("Falling stopped after {} ms. Impact intensity {:.01f}.".format(pkt.duration_ms, pkt.impact_intensity)) 22 | 23 | 24 | def on_button_pressed(cli, pkt: pycozmo.protocol_encoder.ButtonPressed): 25 | if pkt.pressed: 26 | print("Button pressed.") 27 | else: 28 | print("Button released.") 29 | 30 | 31 | def on_robot_picked_up(cli, state: bool): 32 | if state: 33 | print("Picked up.") 34 | else: 35 | print("Put down.") 36 | 37 | 38 | def on_robot_charging(cli, state: bool): 39 | if state: 40 | print("Started charging.") 41 | else: 42 | print("Stopped charging.") 43 | 44 | 45 | def on_cliff_detected(cli, state: bool): 46 | if state: 47 | print("Cliff detected.") 48 | 49 | 50 | def on_robot_wheels_moving(cli, state: bool): 51 | if state: 52 | print("Started moving.") 53 | else: 54 | print("Stopped moving.") 55 | 56 | 57 | def on_robot_orientation_change(cli, orientation: pycozmo.robot.RobotOrientation): 58 | if orientation == pycozmo.robot.RobotOrientation.ON_THREADS: 59 | print("On threads.") 60 | elif orientation == pycozmo.robot.RobotOrientation.ON_BACK: 61 | print("On back.") 62 | elif orientation == pycozmo.robot.RobotOrientation.ON_FACE: 63 | print("On front.") 64 | elif orientation == pycozmo.robot.RobotOrientation.ON_LEFT_SIDE: 65 | print("On left side.") 66 | elif orientation == pycozmo.robot.RobotOrientation.ON_RIGHT_SIDE: 67 | print("On right side.") 68 | 69 | 70 | # Change the robot log level to DEBUG to see robot debug messages related to events. 71 | with pycozmo.connect(enable_animations=False, robot_log_level="INFO") as cli: 72 | 73 | cli.add_handler(pycozmo.protocol_encoder.RobotState, on_robot_state, one_shot=True) 74 | cli.add_handler(pycozmo.protocol_encoder.RobotPoked, on_robot_poked) 75 | cli.add_handler(pycozmo.protocol_encoder.FallingStarted, on_robot_falling_started) 76 | cli.add_handler(pycozmo.protocol_encoder.FallingStopped, on_robot_falling_stopped) 77 | cli.add_handler(pycozmo.protocol_encoder.ButtonPressed, on_button_pressed) 78 | cli.add_handler(pycozmo.event.EvtRobotPickedUpChange, on_robot_picked_up) 79 | cli.add_handler(pycozmo.event.EvtRobotChargingChange, on_robot_charging) 80 | cli.add_handler(pycozmo.event.EvtCliffDetectedChange, on_cliff_detected) 81 | cli.add_handler(pycozmo.event.EvtRobotWheelsMovingChange, on_robot_wheels_moving) 82 | cli.add_handler(pycozmo.event.EvtRobotOrientationChange, on_robot_orientation_change) 83 | 84 | while True: 85 | time.sleep(0.1) 86 | -------------------------------------------------------------------------------- /docs/offboard_functions.md: -------------------------------------------------------------------------------- 1 | 2 | Cozmo Off-Board Functions 3 | ========================= 4 | 5 | Cozmo mobile application resources consist of: 6 | - audio files 7 | - animations 8 | - animation group descriptions 9 | - behaviors 10 | - reaction triggers 11 | - emotions 12 | - activities 13 | - text-to-speech models 14 | 15 | Robot firmware images are also distributed as part of the app resources. 16 | 17 | 18 | Directory structure 19 | ------------------- 20 | 21 | ``` 22 | cozmo_resources/ 23 | assets/ 24 | animationGroupMaps/ 25 | animationGroups/ 26 | animations/ 27 | cubeAnimationGroupMaps/ 28 | faceAnimations/ 29 | RewardedActions/ 30 | config/ 31 | engine/ 32 | animations/ 33 | behaviorSystem/ 34 | activities/ 35 | behaviors/ 36 | emotionevents/ 37 | firmware/ 38 | lights/ 39 | backpackLights/ 40 | cubeLights/ 41 | sound/ 42 | English(US) 43 | tts/ 44 | ``` 45 | 46 | 47 | Audio files 48 | ----------- 49 | 50 | ### WEM files 51 | ### BNK files 52 | 53 | 54 | Animations 55 | ---------- 56 | 57 | Cozmo "animations" allow animating the following aspects of the robot: 58 | 59 | - body movement 60 | - lift movement 61 | - head movement 62 | - face images 63 | - backpack LED animations 64 | - audio 65 | 66 | Cozmo animations are series of keyframes, stored in binary files in [FlatBuffers](https://google.github.io/flatbuffers/) 67 | format. Animation data structures are declared in FlatBuffers format in 68 | `files/cozmo/cozmo_resources/config/cozmo_anim.fbs` . The animation files are available in the following directory of 69 | the Android mobile application: 70 | 71 | `files/cozmo/cozmo_resources/assets/animations` 72 | 73 | Face images are generated procedurally. They are described by 43 parameters - 5 for the face and 19 for each eye. 74 | The face as a whole can be translated, scaled, and rotated. Each individual eye can be translated, scaled, and rotated. 75 | The 4 corners of each eye can be controlled and each eye has a lower and upper lid. 76 | 77 | The following presentation from Anki provides some background information on Cozmo animations: 78 | 79 | [Cozmo: Animation pipeline for a physical robot](https://www.gdcvault.com/play/1024488/Cozmo-Animation-Pipeline-for-a) 80 | 81 | 82 | Animation groups 83 | ---------------- 84 | 85 | Animation groups are sets of animations with the same purpose. 86 | 87 | 88 | Behaviors 89 | --------- 90 | 91 | Behaviors can be thought of as small applications that perform a specific function using the robot client API. 92 | 93 | 94 | Reactions 95 | --------- 96 | 97 | Reactions map robot events to behaviors. 98 | 99 | 100 | Emotions 101 | -------- 102 | 103 | Emotions are modeled as value functions that change in one of the following ways: 104 | - over time, driven by a decay function 105 | - as a result of reactions 106 | - as a result of behaviors 107 | 108 | 109 | Activities 110 | ---------- 111 | 112 | Activities are sets of behaviors with a rule how to choose 113 | -------------------------------------------------------------------------------- /pycozmo/run.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | Helper functions for running PyCozmo applications. 4 | 5 | """ 6 | 7 | from typing import Optional 8 | import sys 9 | import os 10 | import logging 11 | from contextlib import contextmanager 12 | 13 | from . import logger, logger_protocol, logger_robot, logger_reaction, logger_behavior, logger_animation 14 | from . import client 15 | from . import exception 16 | 17 | 18 | __all__ = [ 19 | 'setup_basic_logging', 20 | 'connect', 21 | ] 22 | 23 | 24 | def setup_basic_logging( 25 | log_level: Optional[str] = None, 26 | protocol_log_level: Optional[str] = None, 27 | robot_log_level: Optional[str] = None, 28 | target=sys.stderr) -> None: 29 | 30 | if log_level is None: 31 | log_level = os.environ.get('PYCOZMO_LOG_LEVEL', logging.INFO) 32 | if protocol_log_level is None: 33 | protocol_log_level = os.environ.get('PYCOZMO_PROTOCOL_LOG_LEVEL', logging.INFO) 34 | if robot_log_level is None: 35 | # Keeping the default to WARNING due to "AnimationController.IsReadyToPlay.BufferStarved" messages. 36 | robot_log_level = os.environ.get('PYCOZMO_ROBOT_LOG_LEVEL', logging.WARNING) 37 | handler = logging.StreamHandler(stream=target) 38 | formatter = logging.Formatter( 39 | fmt="%(asctime)s.%(msecs)03d %(name)-20s %(levelname)-8s %(message)s", 40 | datefmt="%Y-%m-%d %H:%M:%S") 41 | handler.setFormatter(formatter) 42 | logger.addHandler(handler) 43 | logger.setLevel(log_level) 44 | logger_protocol.addHandler(handler) 45 | logger_protocol.setLevel(protocol_log_level) 46 | logger_robot.addHandler(handler) 47 | logger_robot.setLevel(robot_log_level) 48 | logger_reaction.addHandler(handler) 49 | logger_reaction.setLevel(robot_log_level) 50 | logger_behavior.addHandler(handler) 51 | logger_behavior.setLevel(robot_log_level) 52 | logger_animation.addHandler(handler) 53 | logger_animation.setLevel(robot_log_level) 54 | 55 | 56 | @contextmanager 57 | def connect( 58 | log_level: Optional[str] = None, 59 | protocol_log_level: Optional[str] = None, 60 | protocol_log_messages: Optional[list] = None, 61 | robot_log_level: Optional[str] = None, 62 | auto_initialize: bool = True, 63 | enable_animations: bool = True, 64 | enable_procedural_face: bool = True) -> client.Client: 65 | 66 | setup_basic_logging(log_level=log_level, protocol_log_level=protocol_log_level, robot_log_level=robot_log_level) 67 | 68 | try: 69 | cli = client.Client( 70 | protocol_log_messages=protocol_log_messages, 71 | auto_initialize=auto_initialize, 72 | enable_animations=enable_animations, 73 | enable_procedural_face=enable_procedural_face) 74 | cli.start() 75 | cli.connect() 76 | cli.wait_for_robot() 77 | except exception.PyCozmoException as e: 78 | logger.error(e) 79 | sys.exit(1) 80 | except KeyboardInterrupt: 81 | logger.info("Interrupted...") 82 | sys.exit(0) 83 | 84 | try: 85 | # Exceptions, generated from the application are intentionally not handled. 86 | yield cli 87 | except KeyboardInterrupt: 88 | logger.info("Interrupted...") 89 | finally: 90 | cli.disconnect() 91 | cli.stop() 92 | 93 | logger.info("Done.") 94 | -------------------------------------------------------------------------------- /tools/pycozmo_replay.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from typing import Optional 4 | import sys 5 | import time 6 | 7 | import dpkt 8 | 9 | import pycozmo 10 | 11 | 12 | class ReplayApp(object): 13 | 14 | def __init__(self, log_messages: Optional[list] = None, replay_messages: Optional[list] = None): 15 | self.log_messages = log_messages 16 | self.frame_count = 0 17 | self.pkts = [] 18 | self.first_ts = None 19 | self.packet_id_filter = pycozmo.filter.Filter() 20 | if replay_messages: 21 | for i in replay_messages: 22 | self.packet_id_filter.deny_ids(pycozmo.protocol_encoder.PACKETS_BY_GROUP[i]) 23 | 24 | def load_engine_pkts(self, fspec): 25 | self.pkts = [] 26 | 27 | self.frame_count = 0 28 | with open(fspec, "rb") as f: 29 | self.first_ts = None 30 | for ts, frame in dpkt.pcap.Reader(f): 31 | if self.first_ts is None: 32 | self.first_ts = ts 33 | eth = dpkt.ethernet.Ethernet(frame) 34 | if eth.type != dpkt.ethernet.ETH_TYPE_IP: 35 | # Skip non-IP frames 36 | continue 37 | ip = eth.data 38 | if ip.p != dpkt.ip.IP_PROTO_UDP: 39 | # Skip non-UDP frames 40 | continue 41 | udp = ip.data 42 | if udp.data[:7] != pycozmo.protocol_declaration.FRAME_ID: 43 | # Skip non-Cozmo frames 44 | continue 45 | frame = pycozmo.Frame.from_bytes(udp.data) 46 | if frame.type not in (pycozmo.protocol_declaration.FrameType.ENGINE, 47 | pycozmo.protocol_declaration.FrameType.ENGINE_ACT): 48 | # Skip non-engine frames 49 | continue 50 | for pkt in frame.pkts: 51 | if pkt.type not in [pycozmo.protocol_declaration.PacketType.COMMAND, 52 | pycozmo.protocol_declaration.PacketType.KEYFRAME]: 53 | # Skip non-command packets 54 | continue 55 | self.pkts.append((ts, pkt)) 56 | self.frame_count += 1 57 | 58 | print("Loaded {} engine packets from {} frames.".format( 59 | len(self.pkts), self.frame_count)) 60 | 61 | def replay(self, fspec): 62 | self.load_engine_pkts(fspec) 63 | 64 | pycozmo.setup_basic_logging(log_level="DEBUG", protocol_log_level="DEBUG") 65 | cli = pycozmo.Client(protocol_log_messages=self.log_messages) 66 | cli.start() 67 | cli.connect() 68 | cli.wait_for_robot() 69 | 70 | try: 71 | for i, v in enumerate(self.pkts): 72 | # if i < 1113: 73 | # continue 74 | ts, pkt = v 75 | if self.packet_id_filter.filter(pkt.id): 76 | continue 77 | input() 78 | print("{}, time={:.06f}".format(i, ts - self.first_ts)) 79 | cli.conn.send(pkt) 80 | except KeyboardInterrupt: 81 | pass 82 | 83 | cli.disconnect() 84 | time.sleep(1) 85 | 86 | 87 | def main(): 88 | fspec = sys.argv[1] 89 | log_messages = [] # "objects", "audio", "state"] 90 | replay_messages = [] # "lights", "objects"] 91 | 92 | app = ReplayApp(log_messages=log_messages, replay_messages=replay_messages) 93 | app.replay(fspec) 94 | 95 | 96 | if __name__ == '__main__': 97 | main() 98 | -------------------------------------------------------------------------------- /pycozmo/lights.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | Helper routines for working with colors and lights. 4 | 5 | """ 6 | 7 | from typing import Optional, Tuple 8 | 9 | from . import protocol_encoder 10 | 11 | 12 | __all__ = [ 13 | 'Color', 14 | 15 | 'green', 16 | 'red', 17 | 'blue', 18 | 'white', 19 | 'off', 20 | 21 | 'green_light', 22 | 'red_light', 23 | 'blue_light', 24 | 'white_light', 25 | 'off_light', 26 | ] 27 | 28 | 29 | LED_ENC_BLUE = 0x001f 30 | LED_ENC_GREEN = 0x03e0 31 | LED_ENC_RED = 0x7c00 32 | LED_ENC_IR = 0x8000 33 | 34 | LED_ENC_BLUE_SHIFT = 0 35 | LED_ENC_GREEN_SHIFT = 5 36 | LED_ENC_RED_SHIFT = 10 37 | LED_ENC_IR_SHIFT = 15 38 | 39 | 40 | class Color: 41 | """ 42 | A Color to be used with a Light. 43 | 44 | Either int_color or rgb may be used to specify the actual color. 45 | Any alpha components (from int_color) are ignored - all colors are fully opaque. 46 | 47 | Args: 48 | int_color (int): A 32 bit value holding the binary RGBA value. 49 | rgb (tuple): A tuple holding the integer values from 0-255 for (red, green, blue) 50 | name (str): A name to assign to this color 51 | """ 52 | 53 | def __init__(self, 54 | int_color: Optional[int] = None, 55 | rgb: Optional[Tuple[int, int, int]] = None, 56 | name: str = Optional[None]) -> None: 57 | self.name = name 58 | if int_color is not None: 59 | self._int_color = int(int_color) | 0xff 60 | elif rgb is not None: 61 | self._int_color = (int(rgb[0]) << 24) | (int(rgb[1]) << 16) | (int(rgb[2]) << 8) | 0xff 62 | else: 63 | self._int_color = 0 64 | 65 | @property 66 | def int_color(self) -> int: 67 | return self._int_color 68 | 69 | def to_int16(self) -> int: 70 | r = ((self._int_color & 0xFF000000) >> 24) * 31 // 255 71 | g = ((self._int_color & 0x00FF0000) >> 16) * 31 // 255 72 | b = ((self._int_color & 0x0000FF00) >> 8) * 31 // 255 73 | value = (r << LED_ENC_RED_SHIFT) | (g << LED_ENC_GREEN_SHIFT) | (b << LED_ENC_BLUE_SHIFT) 74 | return value 75 | 76 | @classmethod 77 | def from_int16(cls, value: int) -> "Color": 78 | r = (value & LED_ENC_RED) >> LED_ENC_RED_SHIFT 79 | g = (value & LED_ENC_GREEN) >> LED_ENC_GREEN_SHIFT 80 | b = (value & LED_ENC_BLUE) >> LED_ENC_BLUE_SHIFT 81 | rgb = ( 82 | r * 255 // 31, 83 | g * 255 // 31, 84 | b * 255 // 31 85 | ) 86 | obj = cls(rgb=rgb) 87 | return obj 88 | 89 | def __repr__(self): 90 | return "Color(name={}, int_color=0x{:08x})".format(self.name, self._int_color) 91 | 92 | 93 | #: Green color. 94 | green = Color(name="green", int_color=0x00ff00ff) 95 | #: Red color. 96 | red = Color(name="red", int_color=0xff0000ff) 97 | #: BLue color. 98 | blue = Color(name="blue", int_color=0x0000ffff) 99 | #: White color. 100 | white = Color(name="white", int_color=0xffffffff) # Does not work well with cubes? 101 | #: Off/no color. 102 | off = Color(name="off") 103 | 104 | #: Green light. 105 | green_light = protocol_encoder.LightState(on_color=green.to_int16(), off_color=green.to_int16()) 106 | #: Red light. 107 | red_light = protocol_encoder.LightState(on_color=red.to_int16(), off_color=red.to_int16()) 108 | #: Blue light. 109 | blue_light = protocol_encoder.LightState(on_color=blue.to_int16(), off_color=blue.to_int16()) 110 | #: White light. 111 | white_light = protocol_encoder.LightState(on_color=white.to_int16(), off_color=white.to_int16()) 112 | #: Off/no light. 113 | off_light = protocol_encoder.LightState(on_color=off.to_int16(), off_color=off.to_int16()) 114 | -------------------------------------------------------------------------------- /docs/source/overview.md: -------------------------------------------------------------------------------- 1 | 2 | Overview 3 | ======== 4 | 5 | https://github.com/zayfod/pycozmo 6 | 7 | `PyCozmo` is a pure-Python communication library, alternative SDK, and application for the 8 | [Cozmo robot](https://www.digitaldreamlabs.com/pages/cozmo) . It allows controlling a Cozmo robot directly, without 9 | having to go through a mobile device, running the Cozmo app. 10 | 11 | The library is loosely based on the [Anki Cozmo Python SDK](https://github.com/anki/cozmo-python-sdk) and the 12 | [cozmoclad](https://pypi.org/project/cozmoclad/) ("C-Like Abstract Data") library. 13 | 14 | This project is a tool for exploring the hardware and software of the Digital Dream Labs (originally Anki) Cozmo robot. 15 | It is unstable and heavily under development. 16 | 17 | 18 | Usage 19 | ----- 20 | 21 | Basic: 22 | 23 | ```python 24 | import time 25 | import pycozmo 26 | 27 | with pycozmo.connect() as cli: 28 | cli.set_head_angle(angle=0.6) 29 | time.sleep(1) 30 | ``` 31 | 32 | Advanced: 33 | 34 | ```python 35 | import pycozmo 36 | 37 | cli = pycozmo.Client() 38 | cli.start() 39 | cli.connect() 40 | cli.wait_for_robot() 41 | 42 | cli.drive_wheels(lwheel_speed=50.0, rwheel_speed=50.0, duration=2.0) 43 | 44 | cli.disconnect() 45 | cli.stop() 46 | ``` 47 | 48 | 49 | PyCozmo vs. the Cozmo SDK 50 | ------------------------- 51 | 52 | A Cozmo SDK application (aka "game") acts as a client to the Cozmo app (aka "engine") that runs on a mobile device. 53 | The low-level communication happens over USB and is handled by the `cozmoclad` library. 54 | 55 | In contrast, an application using PyCozmo basically replaces the Cozmo app and acts as the "engine". PyCozmo handles 56 | the low-level UDP communication with Cozmo. 57 | 58 | ``` 59 | +------------------+ +------------------+ +------------------+ 60 | | SDK app | Cozmo SDK | Cozmo app | Cozmo | Cozmo | 61 | | "game" | cozmoclad | "engine" | protocol | "robot" | 62 | | | ----------------> | Wi-Fi client | ----------------> | Wi-Fi AP | 63 | | | USB | UDP client | UDP/Wi-Fi | UDP Server | 64 | +------------------+ +------------------+ +------------------+ 65 | ``` 66 | 67 | 68 | Requirements 69 | ------------ 70 | 71 | - [Python](https://www.python.org/downloads/) 3.6.0 or newer 72 | - [Pillow](https://github.com/python-pillow/Pillow) 6.0.0 - Python image library 73 | - [FlatBuffers](https://github.com/google/flatbuffers) - serialization library 74 | - [dpkt](https://github.com/kbandla/dpkt) - TCP/IP packet parsing library 75 | 76 | 77 | Installation 78 | ------------ 79 | 80 | Using pip: 81 | 82 | ``` 83 | pip install --user pycozmo 84 | 85 | pycozmo_resources.py download 86 | ``` 87 | 88 | From source: 89 | 90 | ``` 91 | git clone https://github.com/zayfod/pycozmo.git 92 | cd pycozmo 93 | python setup.py install --user 94 | 95 | pycozmo_resources.py download 96 | ``` 97 | 98 | From source, for development: 99 | 100 | ``` 101 | git clone git@github.com:zayfod/pycozmo.git 102 | cd pycozmo 103 | python setup.py develop --user 104 | pip install --user -r requirements-dev.txt 105 | 106 | pycozmo_resources.py download 107 | ``` 108 | 109 | 110 | Support 111 | ------- 112 | 113 | Bug reports and changes should be sent via GitHub: 114 | 115 | [https://github.com/zayfod/pycozmo](https://github.com/zayfod/pycozmo) 116 | 117 | DDL Robot Discord server, channel #development-cozmo: 118 | 119 | [https://discord.gg/ew92haS](https://discord.gg/ew92haS) 120 | 121 | 122 | Disclaimer 123 | ---------- 124 | 125 | This project is not affiliated with [Digital Dream Labs](https://www.digitaldreamlabs.com/) or 126 | [Anki](https://anki.com/). 127 | -------------------------------------------------------------------------------- /pycozmo/CozmoAnim/RobotAudio.py: -------------------------------------------------------------------------------- 1 | # automatically generated by the FlatBuffers compiler, do not modify 2 | 3 | # namespace: CozmoAnim 4 | 5 | import flatbuffers 6 | 7 | class RobotAudio(object): 8 | __slots__ = ['_tab'] 9 | 10 | @classmethod 11 | def GetRootAsRobotAudio(cls, buf, offset): 12 | n = flatbuffers.encode.Get(flatbuffers.packer.uoffset, buf, offset) 13 | x = RobotAudio() 14 | x.Init(buf, n + offset) 15 | return x 16 | 17 | # RobotAudio 18 | def Init(self, buf, pos): 19 | self._tab = flatbuffers.table.Table(buf, pos) 20 | 21 | # RobotAudio 22 | def TriggerTimeMs(self): 23 | o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(4)) 24 | if o != 0: 25 | return self._tab.Get(flatbuffers.number_types.Uint32Flags, o + self._tab.Pos) 26 | return 0 27 | 28 | # RobotAudio 29 | def AudioEventId(self, j): 30 | o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(6)) 31 | if o != 0: 32 | a = self._tab.Vector(o) 33 | return self._tab.Get(flatbuffers.number_types.Int64Flags, a + flatbuffers.number_types.UOffsetTFlags.py_type(j * 8)) 34 | return 0 35 | 36 | # RobotAudio 37 | def AudioEventIdAsNumpy(self): 38 | o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(6)) 39 | if o != 0: 40 | return self._tab.GetVectorAsNumpy(flatbuffers.number_types.Int64Flags, o) 41 | return 0 42 | 43 | # RobotAudio 44 | def AudioEventIdLength(self): 45 | o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(6)) 46 | if o != 0: 47 | return self._tab.VectorLen(o) 48 | return 0 49 | 50 | # RobotAudio 51 | def Volume(self): 52 | o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(8)) 53 | if o != 0: 54 | return self._tab.Get(flatbuffers.number_types.Float32Flags, o + self._tab.Pos) 55 | return 1.0 56 | 57 | # RobotAudio 58 | def Probability(self, j): 59 | o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(10)) 60 | if o != 0: 61 | a = self._tab.Vector(o) 62 | return self._tab.Get(flatbuffers.number_types.Float32Flags, a + flatbuffers.number_types.UOffsetTFlags.py_type(j * 4)) 63 | return 0 64 | 65 | # RobotAudio 66 | def ProbabilityAsNumpy(self): 67 | o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(10)) 68 | if o != 0: 69 | return self._tab.GetVectorAsNumpy(flatbuffers.number_types.Float32Flags, o) 70 | return 0 71 | 72 | # RobotAudio 73 | def ProbabilityLength(self): 74 | o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(10)) 75 | if o != 0: 76 | return self._tab.VectorLen(o) 77 | return 0 78 | 79 | # RobotAudio 80 | def HasAlts(self): 81 | o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(12)) 82 | if o != 0: 83 | return bool(self._tab.Get(flatbuffers.number_types.BoolFlags, o + self._tab.Pos)) 84 | return True 85 | 86 | def RobotAudioStart(builder): builder.StartObject(5) 87 | def RobotAudioAddTriggerTimeMs(builder, triggerTimeMs): builder.PrependUint32Slot(0, triggerTimeMs, 0) 88 | def RobotAudioAddAudioEventId(builder, audioEventId): builder.PrependUOffsetTRelativeSlot(1, flatbuffers.number_types.UOffsetTFlags.py_type(audioEventId), 0) 89 | def RobotAudioStartAudioEventIdVector(builder, numElems): return builder.StartVector(8, numElems, 8) 90 | def RobotAudioAddVolume(builder, volume): builder.PrependFloat32Slot(2, volume, 1.0) 91 | def RobotAudioAddProbability(builder, probability): builder.PrependUOffsetTRelativeSlot(3, flatbuffers.number_types.UOffsetTFlags.py_type(probability), 0) 92 | def RobotAudioStartProbabilityVector(builder, numElems): return builder.StartVector(4, numElems, 4) 93 | def RobotAudioAddHasAlts(builder, hasAlts): builder.PrependBoolSlot(4, hasAlts, 1) 94 | def RobotAudioEnd(builder): return builder.EndObject() 95 | -------------------------------------------------------------------------------- /tools/pycozmo_dump.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import sys 4 | import os 5 | import dpkt 6 | try: 7 | # noinspection PyPackageRequirements 8 | import pcap 9 | except ImportError: 10 | pcap = None 11 | 12 | import pycozmo 13 | 14 | 15 | class DumpApp(object): 16 | 17 | def __init__(self): 18 | self.first_ts = None 19 | self.frame_count = 0 20 | self.filtered_frame_count = 0 21 | self.packet_count = 0 22 | self.unknown_count = 0 23 | self.filtered_packet_count = 0 24 | self.frame_type_filter = pycozmo.filter.Filter() 25 | # self.frame_type_filter.deny_ids({pycozmo.protocol_declaration.FrameType.PING.value}) 26 | self.packet_type_filter = pycozmo.filter.Filter() 27 | # self.packet_type_filter.deny_ids({pycozmo.protocol_declaration.PacketType.PING.value}) 28 | self.packet_id_filter = pycozmo.filter.Filter() 29 | # self.packet_id_filter.deny_ids({0x8f, 0x97}) 30 | 31 | def decode_cozmo_frame(self, ts, buffer): 32 | frame = pycozmo.Frame.from_bytes(buffer) 33 | 34 | if self.frame_type_filter.filter(frame.type.value): 35 | self.filtered_frame_count += 1 36 | return 37 | 38 | print("{:<12s}first_seq=0x{:04x}, seq=0x{:04x}, ack=0x{:04x}, frame={:6d}, time={:.06f}".format( 39 | frame.type.name, frame.first_seq, frame.seq, frame.ack, self.frame_count + 1, ts)) 40 | 41 | for pkt in frame.pkts: 42 | self.packet_count += 1 43 | if isinstance(pkt, pycozmo.protocol_base.UnknownPacket): 44 | self.unknown_count += 1 45 | if self.packet_type_filter.filter(pkt.type.value): 46 | self.filtered_packet_count += 1 47 | continue 48 | if self.packet_id_filter.filter(pkt.id): 49 | self.filtered_packet_count += 1 50 | continue 51 | direction = "->" if pkt.is_from_robot() else "<-" 52 | print("\t{} time={:.06f} {}".format(direction, ts, pkt)) 53 | # if ts > 15: 54 | # sys.exit(1) 55 | 56 | def handle_frame(self, ts, frame): 57 | if self.first_ts is None: 58 | self.first_ts = ts 59 | rel_ts = ts - self.first_ts 60 | eth = dpkt.ethernet.Ethernet(frame) 61 | if eth.type != dpkt.ethernet.ETH_TYPE_IP: 62 | # Skip non-IP frames 63 | return 64 | ip = eth.data 65 | if ip.p != dpkt.ip.IP_PROTO_UDP: 66 | # Skip non-UDP frames 67 | return 68 | udp = ip.data 69 | if udp.data[:7] != pycozmo.protocol_declaration.FRAME_ID: 70 | # Skip non-Cozmo frames 71 | return 72 | self.decode_cozmo_frame(rel_ts, udp.data) 73 | self.frame_count += 1 74 | # if frame_count > 100: 75 | # break 76 | 77 | def decode_pcap(self, fspec): 78 | with open(fspec, "rb") as f: 79 | for ts, frame in dpkt.pcap.Reader(f): 80 | self.handle_frame(ts, frame) 81 | 82 | print() 83 | print("Frames: {}".format(self.frame_count)) 84 | print("Packets: {}".format(self.packet_count)) 85 | if self.packet_count: 86 | print("Unknown packets: {} ({:.0f}%)".format( 87 | self.unknown_count, 100.0*self.unknown_count/self.packet_count)) 88 | 89 | def capture(self, interface): 90 | pc = pcap.pcap(name=interface) 91 | # pc.setfilter('') 92 | print('listening on %s: %s' % (pc.name, pc.filter)) 93 | pc.loop(0, self.handle_frame) 94 | 95 | 96 | def main(): 97 | fspec = sys.argv[1] 98 | 99 | app = DumpApp() 100 | 101 | if os.path.exists(fspec): 102 | app.decode_pcap(fspec) 103 | else: 104 | if pcap is None: 105 | sys.exit("Live packet capturing requires pypcap. Install with 'pip install --user pypcap'.") 106 | 107 | app.capture(fspec) 108 | 109 | 110 | if __name__ == '__main__': 111 | main() 112 | -------------------------------------------------------------------------------- /pycozmo/CozmoAnim/TurnToRecordedHeading.py: -------------------------------------------------------------------------------- 1 | # automatically generated by the FlatBuffers compiler, do not modify 2 | 3 | # namespace: CozmoAnim 4 | 5 | import flatbuffers 6 | 7 | class TurnToRecordedHeading(object): 8 | __slots__ = ['_tab'] 9 | 10 | @classmethod 11 | def GetRootAsTurnToRecordedHeading(cls, buf, offset): 12 | n = flatbuffers.encode.Get(flatbuffers.packer.uoffset, buf, offset) 13 | x = TurnToRecordedHeading() 14 | x.Init(buf, n + offset) 15 | return x 16 | 17 | # TurnToRecordedHeading 18 | def Init(self, buf, pos): 19 | self._tab = flatbuffers.table.Table(buf, pos) 20 | 21 | # TurnToRecordedHeading 22 | def TriggerTimeMs(self): 23 | o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(4)) 24 | if o != 0: 25 | return self._tab.Get(flatbuffers.number_types.Uint32Flags, o + self._tab.Pos) 26 | return 0 27 | 28 | # TurnToRecordedHeading 29 | def DurationTimeMs(self): 30 | o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(6)) 31 | if o != 0: 32 | return self._tab.Get(flatbuffers.number_types.Uint32Flags, o + self._tab.Pos) 33 | return 0 34 | 35 | # TurnToRecordedHeading 36 | def OffsetDeg(self): 37 | o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(8)) 38 | if o != 0: 39 | return self._tab.Get(flatbuffers.number_types.Int16Flags, o + self._tab.Pos) 40 | return 0 41 | 42 | # TurnToRecordedHeading 43 | def SpeedDegPerSec(self): 44 | o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(10)) 45 | if o != 0: 46 | return self._tab.Get(flatbuffers.number_types.Int16Flags, o + self._tab.Pos) 47 | return 0 48 | 49 | # TurnToRecordedHeading 50 | def AccelDegPerSec2(self): 51 | o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(12)) 52 | if o != 0: 53 | return self._tab.Get(flatbuffers.number_types.Int16Flags, o + self._tab.Pos) 54 | return 1000 55 | 56 | # TurnToRecordedHeading 57 | def DecelDegPerSec2(self): 58 | o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(14)) 59 | if o != 0: 60 | return self._tab.Get(flatbuffers.number_types.Int16Flags, o + self._tab.Pos) 61 | return 1000 62 | 63 | # TurnToRecordedHeading 64 | def ToleranceDeg(self): 65 | o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(16)) 66 | if o != 0: 67 | return self._tab.Get(flatbuffers.number_types.Uint16Flags, o + self._tab.Pos) 68 | return 2 69 | 70 | # TurnToRecordedHeading 71 | def NumHalfRevs(self): 72 | o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(18)) 73 | if o != 0: 74 | return self._tab.Get(flatbuffers.number_types.Uint16Flags, o + self._tab.Pos) 75 | return 0 76 | 77 | # TurnToRecordedHeading 78 | def UseShortestDir(self): 79 | o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(20)) 80 | if o != 0: 81 | return bool(self._tab.Get(flatbuffers.number_types.BoolFlags, o + self._tab.Pos)) 82 | return False 83 | 84 | def TurnToRecordedHeadingStart(builder): builder.StartObject(9) 85 | def TurnToRecordedHeadingAddTriggerTimeMs(builder, triggerTimeMs): builder.PrependUint32Slot(0, triggerTimeMs, 0) 86 | def TurnToRecordedHeadingAddDurationTimeMs(builder, durationTimeMs): builder.PrependUint32Slot(1, durationTimeMs, 0) 87 | def TurnToRecordedHeadingAddOffsetDeg(builder, offsetDeg): builder.PrependInt16Slot(2, offsetDeg, 0) 88 | def TurnToRecordedHeadingAddSpeedDegPerSec(builder, speedDegPerSec): builder.PrependInt16Slot(3, speedDegPerSec, 0) 89 | def TurnToRecordedHeadingAddAccelDegPerSec2(builder, accelDegPerSec2): builder.PrependInt16Slot(4, accelDegPerSec2, 1000) 90 | def TurnToRecordedHeadingAddDecelDegPerSec2(builder, decelDegPerSec2): builder.PrependInt16Slot(5, decelDegPerSec2, 1000) 91 | def TurnToRecordedHeadingAddToleranceDeg(builder, toleranceDeg): builder.PrependUint16Slot(6, toleranceDeg, 2) 92 | def TurnToRecordedHeadingAddNumHalfRevs(builder, numHalfRevs): builder.PrependUint16Slot(7, numHalfRevs, 0) 93 | def TurnToRecordedHeadingAddUseShortestDir(builder, useShortestDir): builder.PrependBoolSlot(8, useShortestDir, 0) 94 | def TurnToRecordedHeadingEnd(builder): return builder.EndObject() 95 | -------------------------------------------------------------------------------- /tools/pycozmo_resources.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | 4 | Cozmo resource manager. 5 | 6 | """ 7 | 8 | import sys 9 | import os 10 | import urllib.request 11 | import ssl 12 | import zipfile 13 | import argparse 14 | import pathlib 15 | import shutil 16 | 17 | import pycozmo 18 | 19 | ssl._create_default_https_context = ssl._create_unverified_context # noqa 20 | 21 | 22 | OBB_URL = "https://media.githubusercontent.com/media/cristobalraya/cozmo-archive/" \ 23 | "master/applications/com.anki.cozmo_3.4.0-1204_plus_OBB.zip" 24 | 25 | 26 | def download(fspec: pathlib.Path) -> bool: 27 | try: 28 | with urllib.request.urlopen(OBB_URL) as response, open(fspec, "wb") as f: 29 | while True: 30 | data = response.read(8192) 31 | if not data: 32 | break 33 | f.write(data) 34 | return True 35 | except Exception: # noqa 36 | return False 37 | 38 | 39 | def extract(fspec: pathlib.Path, dspec: pathlib.Path) -> bool: 40 | """ Extract an archive to a directory. """ 41 | try: 42 | os.makedirs(str(dspec)) 43 | except FileExistsError: 44 | pass 45 | try: 46 | with zipfile.ZipFile(str(fspec), "r") as f: 47 | f.extractall(str(dspec)) 48 | return True 49 | except Exception: # noqa 50 | return False 51 | 52 | 53 | def do_status() -> None: 54 | """ Check whether resources are available. """ 55 | asset_dir = pycozmo.util.get_cozmo_asset_dir() 56 | if os.path.exists(asset_dir / "resources.txt"): 57 | print(f"Resources found in {asset_dir}") 58 | else: 59 | print(f"Resources NOT found in {asset_dir}") 60 | sys.exit(1) 61 | 62 | 63 | def do_download() -> None: 64 | """ Download and extract resources. """ 65 | 66 | asset_dir = pycozmo.util.get_cozmo_asset_dir() 67 | resource_file = asset_dir / "obb.zip" 68 | 69 | # Check whether resources have already been downloaded. 70 | if os.path.exists(asset_dir / "resources.txt"): 71 | print(f"Resources already available in {asset_dir}") 72 | sys.exit(1) 73 | 74 | # Create directory structure. 75 | try: 76 | os.makedirs(asset_dir) 77 | except FileExistsError: 78 | pass 79 | 80 | print("Downloading...") 81 | 82 | res = download(resource_file) 83 | if not res: 84 | print("ERROR: Download failed.") 85 | sys.exit(2) 86 | 87 | print("Extracting...") 88 | 89 | res = extract( 90 | resource_file, 91 | asset_dir / "obb") 92 | if not res: 93 | print("ERROR: Extraction failed.") 94 | sys.exit(3) 95 | os.remove(str(resource_file)) 96 | 97 | res = extract( 98 | asset_dir / "obb" / "Android" / "obb" / "com.anki.cozmo" / "main.1204.com.anki.cozmo.obb", 99 | asset_dir / "..") 100 | if not res: 101 | print("ERROR: Secondary extraction failed.") 102 | sys.exit(4) 103 | shutil.rmtree(asset_dir / "obb") 104 | 105 | res = extract( 106 | asset_dir / "cozmo_resources" / "sound" / "AudioAssets.zip", 107 | asset_dir / "cozmo_resources" / "sound") 108 | if not res: 109 | print("ERROR: Sound extraction failed.") 110 | sys.exit(5) 111 | 112 | print(f"Resources downloaded successfully in {asset_dir}") 113 | 114 | 115 | def do_remove() -> None: 116 | """ Remove resources. """ 117 | 118 | asset_dir = pycozmo.util.get_cozmo_asset_dir() 119 | 120 | # Check whether resources exist. 121 | if not os.path.exists(asset_dir / "resources.txt"): 122 | print(f"Resources NOT available in {asset_dir}") 123 | sys.exit(1) 124 | 125 | shutil.rmtree(asset_dir) 126 | 127 | print(f"Resources successfully removed from {asset_dir}") 128 | 129 | 130 | def parse_args() -> argparse.Namespace: 131 | """ Parse command-line arguments. """ 132 | parser = argparse.ArgumentParser(description=__doc__) 133 | subparsers = parser.add_subparsers(dest="cmd") 134 | subparsers.add_parser("status", help="show resource status") 135 | subparsers.add_parser("download", help="download and extract resources") 136 | subparsers.add_parser("remove", help="remove resources") 137 | args = parser.parse_args() 138 | if not args.cmd: 139 | print(parser.format_usage()) 140 | sys.exit(1) 141 | return args 142 | 143 | 144 | def main(): 145 | 146 | # Parse command-line. 147 | args = parse_args() 148 | 149 | if args.cmd == "status": 150 | do_status() 151 | elif args.cmd == "download": 152 | do_download() 153 | elif args.cmd == "remove": 154 | do_remove() 155 | else: 156 | assert False 157 | 158 | 159 | if __name__ == '__main__': 160 | main() 161 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | Revision History 2 | ================ 3 | 4 | v0.8.0 (Nov 12, 2020) 5 | --------------------- 6 | - New animation controller that synchronizes animations, audio playback, and image displaying. 7 | - Procedural face generation to bring the roboto to life. 8 | - Loading of Cozmo resource files - activities, behaviors, emotions, light animations 9 | (thanks to Aitor Miguel Blanco / gimait) 10 | - pycozmo.audiokinetic module for working with Audiokinetic WWise SoundBank files. 11 | - New tool for managing Cozmo resources - pycozmo_resources.py . 12 | - Initial Cozmo application with rudimentary reactions and behaviors - pycozmo_app.py . 13 | - Cozmo protocol client robustness improvements. 14 | - CLAD encoding optimizations. 15 | - Cliff detection and procedural face rendering improvements (thanks to Aitor Miguel Blanco / gimait) 16 | - Replaced pycozmo.run_program() with pycozmo.connect() context manager. 17 | - Renamed the NextFrame packet to OutputSilence to better describe its function. 18 | - Dropped support for Python 3.5 and added support for Python 3.9. 19 | - Bug fixes and documentation improvements. 20 | 21 | v0.7.0 (Sep 26, 2020) 22 | --------------------- 23 | - Full robot audio support and a new AudioManager class (thanks to Aitor Miguel Blanco / gimait). 24 | - Cozmo protocol client robustness improvements (thanks to Aitor Miguel Blanco / gimait): 25 | - frame retransmission 26 | - transmission of multiple packets in a single frame 27 | - Procedural face rendering improvements (thanks to Catherine Chambers / ca2-chambers). 28 | - Added support for robot firmware debug message decoding. 29 | - Added a new video.py example. 30 | - Added a tool for over-the-air firmware updates - pycozmo_update.py (thanks to Einfari). 31 | - Added hardware version description. 32 | - Bug fixes and documentation improvements. 33 | 34 | v0.6.0 (Jan 2, 2020) 35 | -------------------- 36 | - Improved localization - SetOrigin and SyncTime commands and pose (position and orientation) interpretation. 37 | - Added new path tracking commands (AppendPath*, ExecutePath, etc.) and examples (path.py, go_to_pose.py). 38 | - Added support for drawing procedural faces (thanks to Pedro Tiago Pereira / ppedro74). 39 | - Added support for reading and writing animations in FlatBuffers (.bin) and JSON format. 40 | - Added a new tool for examining and manipulating animation files - pycozmo_anim.py . 41 | - Added commands for working with cube/object accelerometers - StreamObjectAccel, ObjectAccel. 42 | - Improved function description. 43 | - Bug fixes and documentation improvements. 44 | 45 | v0.5.0 (Oct 12, 2019) 46 | --------------------- 47 | - Added initial client API. 48 | - Separated low-level Cozmo connection handling into a new ClientConnection class. 49 | - Improved the ImageDecoder class and added a new ImageEncoder class for handling Cozmo protocol image encoding. 50 | - Added new examples for displaying images from files and drawing on Cozmo's OLED display. 51 | - New protocol commands: EnableBodyACC, EnableAnimationState, AnimHead, AnimLift, AnimBackpackLights, AnimBody, 52 | StartAnimation, EndAnimation, AnimationStarted, AnimationEnded, DebugData. 53 | - Initial support for Cozmo animations in FlatBuffer .bin files. 54 | - Improved filtering through packet groups for pycozmo_dump.py and pycozmo_replay.py . 55 | - Added type hints in the protocol generator. 56 | - Bug fixes and documentation improvements. 57 | 58 | v0.4.0 (Sep 13, 2019) 59 | --------------------- 60 | - New commands: Enable, TurnInPlace, DriveStraight, ButtonPressed, HardwareInfo, BodyInfo, EnableColorImages, 61 | EnableStopOnCliff, NvStorageOp, NvStorageOpResult, FirmwareUpdate, FirmwareUpdateResult. 62 | - New events: AnimationState, ObjectAvailable, ImageIMUData. 63 | - New examples: cube_lights.py, charger_lights.py, cube_light_animation.py. 64 | - Improved handling of 0x04 frames 65 | - Added support for Int8, Int32, and enumeration packet fields. 66 | - Improved robot state access. 67 | - Added object availability and animation state access. 68 | - Added initial pycozmo_replay.py tool for replaying PCAP files to Cozmo. 69 | - Added OLED display initial image encoder code. 70 | - Added initial function description. 71 | 72 | v0.3.0 (Sep 1, 2019) 73 | -------------------- 74 | - Camera control and image reconstruction commands. 75 | - Initial robot state commands (coordinates, orientation, track speed, battery voltage). 76 | - Cube control commands. 77 | - Fall detection commands. 78 | - Audio volume control command. 79 | - Firmware signature identification commands. 80 | - Improved logging control. 81 | - Python 3.5 compatibility fixes (thanks to Cyke). 82 | 83 | v0.2.0 (Aug 25, 2019) 84 | --------------------- 85 | - Backpack light control commands and example. 86 | - Raw display control commands and example. 87 | - Audio output commands and example. 88 | 89 | v0.1.0 (Aug 15, 2019) 90 | --------------------- 91 | - Initial release. 92 | -------------------------------------------------------------------------------- /pycozmo/protocol_base.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | Cozmo protocol implementation base. 4 | 5 | """ 6 | 7 | from typing import Optional 8 | from abc import ABC, abstractmethod 9 | 10 | from .protocol_ast import PacketType 11 | from .protocol_declaration import FIRST_ROBOT_PACKET_ID, OOB_SEQ 12 | from .protocol_utils import BinaryReader, BinaryWriter 13 | from .util import hex_dump 14 | 15 | 16 | __all__ = [ 17 | "Struct", 18 | "Packet", 19 | "UnknownPacket", 20 | "UnknownCommand", 21 | "UnknownEvent", 22 | ] 23 | 24 | 25 | class Struct(ABC): 26 | 27 | @abstractmethod 28 | def __len__(self): 29 | raise NotImplementedError 30 | 31 | @abstractmethod 32 | def __repr__(self): 33 | raise NotImplementedError 34 | 35 | @abstractmethod 36 | def to_bytes(self) -> bytes: 37 | raise NotImplementedError 38 | 39 | @abstractmethod 40 | def to_writer(self, writer: BinaryWriter) -> None: 41 | raise NotImplementedError 42 | 43 | @classmethod 44 | @abstractmethod 45 | def from_bytes(cls, buffer: bytes) -> "Struct": 46 | raise NotImplementedError 47 | 48 | @classmethod 49 | @abstractmethod 50 | def from_reader(cls, reader: BinaryReader) -> "Struct": 51 | raise NotImplementedError 52 | 53 | 54 | class Packet(Struct, ABC): 55 | 56 | __slots__ = ( 57 | "_type", 58 | "_id", 59 | "seq", 60 | "ack", 61 | ) 62 | 63 | def __init__(self, packet_type: PacketType, packet_id: Optional[int] = None): 64 | self.type = packet_type 65 | self.id = packet_id 66 | self.seq = OOB_SEQ 67 | self.ack = OOB_SEQ 68 | 69 | @property 70 | def type(self) -> PacketType: 71 | return self._type 72 | 73 | @type.setter 74 | def type(self, value: PacketType) -> None: 75 | self._type = PacketType(value) 76 | 77 | @property 78 | def id(self) -> Optional[int]: 79 | return self._id 80 | 81 | @id.setter 82 | def id(self, value: Optional[int]) -> None: 83 | self._id = value 84 | 85 | def is_oob(self) -> bool: 86 | res = self.type.value >= PacketType.EVENT.value # type: bool 87 | return res 88 | 89 | def is_from_robot(self) -> bool: 90 | if self.id is not None: 91 | res = self.id >= FIRST_ROBOT_PACKET_ID 92 | else: 93 | res = self.type == PacketType.CONNECT or (self.type == PacketType.PING and self.seq > 0) 94 | return res 95 | 96 | def is_from_engine(self) -> bool: 97 | return not self.is_from_robot() 98 | 99 | 100 | class UnknownPacket(Packet): 101 | 102 | __slots__ = ( 103 | "_data", 104 | ) 105 | 106 | def __init__(self, packet_type: PacketType, data: bytes, packet_id: Optional[int] = None): 107 | super().__init__(packet_type, packet_id) 108 | self.data = data 109 | 110 | @property 111 | def data(self) -> bytes: 112 | return self._data 113 | 114 | @data.setter 115 | def data(self, value: bytes) -> None: 116 | self._data = bytes(value) 117 | 118 | def __len__(self): 119 | return len(self._data) 120 | 121 | def __repr__(self): 122 | return "{type_name}({type:02x}, {data})".format( 123 | type=self.type.value, type_name=type(self).__name__, data=hex_dump(data=self._data)) 124 | 125 | def to_bytes(self): 126 | writer = BinaryWriter() 127 | self.to_writer(writer) 128 | return writer.dumps() 129 | 130 | def to_writer(self, writer): 131 | writer.write_farray(self._data, "B", len(self._data)) 132 | 133 | @classmethod 134 | def from_bytes(cls, buffer): 135 | # The size is not known. 136 | raise NotImplementedError 137 | 138 | @classmethod 139 | def from_reader(cls, reader): 140 | # The size is not known. 141 | raise NotImplementedError 142 | 143 | 144 | class UnknownCommand(UnknownPacket): 145 | 146 | def __init__(self, packet_id: int, data: bytes = b""): 147 | super().__init__(PacketType.COMMAND, data, packet_id=packet_id) 148 | 149 | @classmethod 150 | def from_bytes(cls, buffer): 151 | # The size is not known. 152 | raise NotImplementedError 153 | 154 | @classmethod 155 | def from_reader(cls, reader): 156 | # The size is not known. 157 | raise NotImplementedError 158 | 159 | def __repr__(self): 160 | return "{type}({id:02x}, {data})".format( 161 | id=self.id, type=type(self).__name__, data=hex_dump(data=self._data)) 162 | 163 | 164 | class UnknownEvent(UnknownPacket): 165 | 166 | def __init__(self, packet_id: int, data: bytes = b""): 167 | super().__init__(PacketType.EVENT, data, packet_id=packet_id) 168 | 169 | @classmethod 170 | def from_bytes(cls, buffer): 171 | # The size is not known. 172 | raise NotImplementedError 173 | 174 | @classmethod 175 | def from_reader(cls, reader): 176 | # The size is not known. 177 | raise NotImplementedError 178 | 179 | def __repr__(self): 180 | return "{type}({id:02x}, {data})".format( 181 | id=self.id, type=type(self).__name__, data=hex_dump(data=self._data)) 182 | -------------------------------------------------------------------------------- /tools/pycozmo_anim.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | 4 | Cozmo animation manipulation tool. 5 | 6 | Examples: 7 | 8 | - view clips and keyframes in an animation file 9 | 10 | pycozmo_anim.py info anim_bored_01.bin 11 | 12 | - convert a FlatBuffers (.bin) animation to JSON format 13 | 14 | pycozmo_anim.py json anim_bored_01.bin 15 | 16 | - convert a JSON animation to FlatBuffers format 17 | 18 | pycozmo_anim.py bin anim_bored_01.json 19 | 20 | - export procedural face images from an animation file 21 | 22 | pycozmo_anim.py images anim_bored_01.bin 23 | 24 | """ 25 | 26 | import sys 27 | import os 28 | import argparse 29 | import struct 30 | import json 31 | from collections import Counter 32 | 33 | import pycozmo 34 | 35 | 36 | def do_info(args) -> None: 37 | ifspec = args.input 38 | 39 | try: 40 | clips = pycozmo.anim_encoder.AnimClips.from_fb_file(ifspec) 41 | except (OSError, struct.error) as e: 42 | print("ERROR: Failed to load animation file '{}'. {}".format(ifspec, e)) 43 | sys.exit(1) 44 | 45 | for i, clip in enumerate(clips.clips): 46 | print("{} (index {})".format(clip.name, i)) 47 | counts = Counter([type(keyframe).__name__ for keyframe in clip.keyframes]) 48 | for k, v in sorted(counts.items()): 49 | print("\t{}: {}".format(k, v)) 50 | 51 | 52 | def do_json(args) -> None: 53 | ifspec = args.input 54 | ofspec = args.output 55 | if not ofspec: 56 | ofspec = os.path.splitext(ifspec)[0] + '.json' 57 | 58 | try: 59 | clips = pycozmo.anim_encoder.AnimClips.from_fb_file(ifspec) 60 | except (OSError, struct.error) as e: 61 | print("ERROR: Failed to load animation file '{}'. {}".format(ifspec, e)) 62 | sys.exit(1) 63 | 64 | try: 65 | clips.to_json_file(ofspec) 66 | except OSError as e: 67 | print("ERROR: Failed to write animation file '{}'. {}".format(ifspec, e)) 68 | sys.exit(1) 69 | 70 | 71 | def do_bin(args) -> None: 72 | ifspec = args.input 73 | ofspec = args.output 74 | if not ofspec: 75 | ofspec = os.path.splitext(ifspec)[0] + '.bin' 76 | 77 | try: 78 | clips = pycozmo.anim_encoder.AnimClips.from_json_file(ifspec) 79 | except (OSError, json.decoder.JSONDecodeError) as e: 80 | print("ERROR: Failed to load animation file '{}'. {}".format(ifspec, e)) 81 | sys.exit(1) 82 | 83 | try: 84 | clips.to_fb_file(ofspec) 85 | except OSError as e: 86 | print("ERROR: Failed to write animation file '{}'. {}".format(ifspec, e)) 87 | sys.exit(1) 88 | 89 | 90 | def write_procedural_face(keyframe: pycozmo.anim_encoder.AnimProceduralFace, fspec: str) -> None: 91 | im = pycozmo.anim.PreprocessedClip.keyframe_to_im(keyframe) 92 | try: 93 | im.save(fspec, "PNG") 94 | except OSError as e: 95 | print("ERROR: Failed to write procedural face image '{}'. {}".format(fspec, e)) 96 | sys.exit(1) 97 | 98 | 99 | def do_images(args) -> None: 100 | ifspec = args.input 101 | prefix = args.prefix 102 | if not prefix: 103 | prefix = os.path.splitext(ifspec)[0] 104 | 105 | try: 106 | clips = pycozmo.anim_encoder.AnimClips.from_fb_file(ifspec) 107 | except (OSError, struct.error) as e: 108 | print("ERROR: Failed to load animation file '{}'. {}".format(ifspec, e)) 109 | sys.exit(1) 110 | 111 | for clip in clips.clips: 112 | for keyframe in clip.keyframes: 113 | if isinstance(keyframe, pycozmo.anim_encoder.AnimProceduralFace): 114 | ofspec = "{}-{}-{:06d}.png".format(prefix, clip.name, keyframe.trigger_time_ms) 115 | write_procedural_face(keyframe, ofspec) 116 | 117 | 118 | def parse_arguments(): 119 | parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) 120 | subparsers = parser.add_subparsers(dest="cmd", required=True) 121 | 122 | subparser = subparsers.add_parser("info", help="view clips and keyframes in an animation file") 123 | subparser.add_argument("input", help="input file specification") 124 | 125 | subparser = subparsers.add_parser( 126 | "json", help="convert an animation in FlatBuffers format to JSON format") 127 | subparser.add_argument("input", help="input file specification") 128 | subparser.add_argument("-o", "--output", help="output file specification") 129 | 130 | subparser = subparsers.add_parser( 131 | "bin", help="convert an animation in JSON format to FlatBuffers format.") 132 | subparser.add_argument("input", help="input file specification") 133 | subparser.add_argument("-o", "--output", help="output file specification") 134 | 135 | subparser = subparsers.add_parser("images", help="export procedural face images from an animation file") 136 | subparser.add_argument("input", help="input file specification") 137 | subparser.add_argument("-p", "--prefix", help="output file specification prefix") 138 | 139 | args = parser.parse_args() 140 | return args 141 | 142 | 143 | def main(): 144 | args = parse_arguments() 145 | cmd_func = getattr(sys.modules[__name__], "do_" + args.cmd) 146 | cmd_func(args) 147 | 148 | 149 | if __name__ == '__main__': 150 | main() 151 | -------------------------------------------------------------------------------- /pycozmo/CozmoAnim/ProceduralFace.py: -------------------------------------------------------------------------------- 1 | # automatically generated by the FlatBuffers compiler, do not modify 2 | 3 | # namespace: CozmoAnim 4 | 5 | import flatbuffers 6 | 7 | class ProceduralFace(object): 8 | __slots__ = ['_tab'] 9 | 10 | @classmethod 11 | def GetRootAsProceduralFace(cls, buf, offset): 12 | n = flatbuffers.encode.Get(flatbuffers.packer.uoffset, buf, offset) 13 | x = ProceduralFace() 14 | x.Init(buf, n + offset) 15 | return x 16 | 17 | # ProceduralFace 18 | def Init(self, buf, pos): 19 | self._tab = flatbuffers.table.Table(buf, pos) 20 | 21 | # ProceduralFace 22 | def TriggerTimeMs(self): 23 | o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(4)) 24 | if o != 0: 25 | return self._tab.Get(flatbuffers.number_types.Uint32Flags, o + self._tab.Pos) 26 | return 0 27 | 28 | # ProceduralFace 29 | def FaceAngle(self): 30 | o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(6)) 31 | if o != 0: 32 | return self._tab.Get(flatbuffers.number_types.Float32Flags, o + self._tab.Pos) 33 | return 0.0 34 | 35 | # ProceduralFace 36 | def FaceCenterX(self): 37 | o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(8)) 38 | if o != 0: 39 | return self._tab.Get(flatbuffers.number_types.Float32Flags, o + self._tab.Pos) 40 | return 0.0 41 | 42 | # ProceduralFace 43 | def FaceCenterY(self): 44 | o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(10)) 45 | if o != 0: 46 | return self._tab.Get(flatbuffers.number_types.Float32Flags, o + self._tab.Pos) 47 | return 0.0 48 | 49 | # ProceduralFace 50 | def FaceScaleX(self): 51 | o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(12)) 52 | if o != 0: 53 | return self._tab.Get(flatbuffers.number_types.Float32Flags, o + self._tab.Pos) 54 | return 1.0 55 | 56 | # ProceduralFace 57 | def FaceScaleY(self): 58 | o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(14)) 59 | if o != 0: 60 | return self._tab.Get(flatbuffers.number_types.Float32Flags, o + self._tab.Pos) 61 | return 1.0 62 | 63 | # ProceduralFace 64 | def LeftEye(self, j): 65 | o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(16)) 66 | if o != 0: 67 | a = self._tab.Vector(o) 68 | return self._tab.Get(flatbuffers.number_types.Float32Flags, a + flatbuffers.number_types.UOffsetTFlags.py_type(j * 4)) 69 | return 0 70 | 71 | # ProceduralFace 72 | def LeftEyeAsNumpy(self): 73 | o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(16)) 74 | if o != 0: 75 | return self._tab.GetVectorAsNumpy(flatbuffers.number_types.Float32Flags, o) 76 | return 0 77 | 78 | # ProceduralFace 79 | def LeftEyeLength(self): 80 | o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(16)) 81 | if o != 0: 82 | return self._tab.VectorLen(o) 83 | return 0 84 | 85 | # ProceduralFace 86 | def RightEye(self, j): 87 | o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(18)) 88 | if o != 0: 89 | a = self._tab.Vector(o) 90 | return self._tab.Get(flatbuffers.number_types.Float32Flags, a + flatbuffers.number_types.UOffsetTFlags.py_type(j * 4)) 91 | return 0 92 | 93 | # ProceduralFace 94 | def RightEyeAsNumpy(self): 95 | o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(18)) 96 | if o != 0: 97 | return self._tab.GetVectorAsNumpy(flatbuffers.number_types.Float32Flags, o) 98 | return 0 99 | 100 | # ProceduralFace 101 | def RightEyeLength(self): 102 | o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(18)) 103 | if o != 0: 104 | return self._tab.VectorLen(o) 105 | return 0 106 | 107 | def ProceduralFaceStart(builder): builder.StartObject(8) 108 | def ProceduralFaceAddTriggerTimeMs(builder, triggerTimeMs): builder.PrependUint32Slot(0, triggerTimeMs, 0) 109 | def ProceduralFaceAddFaceAngle(builder, faceAngle): builder.PrependFloat32Slot(1, faceAngle, 0.0) 110 | def ProceduralFaceAddFaceCenterX(builder, faceCenterX): builder.PrependFloat32Slot(2, faceCenterX, 0.0) 111 | def ProceduralFaceAddFaceCenterY(builder, faceCenterY): builder.PrependFloat32Slot(3, faceCenterY, 0.0) 112 | def ProceduralFaceAddFaceScaleX(builder, faceScaleX): builder.PrependFloat32Slot(4, faceScaleX, 1.0) 113 | def ProceduralFaceAddFaceScaleY(builder, faceScaleY): builder.PrependFloat32Slot(5, faceScaleY, 1.0) 114 | def ProceduralFaceAddLeftEye(builder, leftEye): builder.PrependUOffsetTRelativeSlot(6, flatbuffers.number_types.UOffsetTFlags.py_type(leftEye), 0) 115 | def ProceduralFaceStartLeftEyeVector(builder, numElems): return builder.StartVector(4, numElems, 4) 116 | def ProceduralFaceAddRightEye(builder, rightEye): builder.PrependUOffsetTRelativeSlot(7, flatbuffers.number_types.UOffsetTFlags.py_type(rightEye), 0) 117 | def ProceduralFaceStartRightEyeVector(builder, numElems): return builder.StartVector(4, numElems, 4) 118 | def ProceduralFaceEnd(builder): return builder.EndObject() 119 | -------------------------------------------------------------------------------- /pycozmo/emotions.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | Emotion representation and reading. 4 | 5 | """ 6 | 7 | import os 8 | import time 9 | from typing import Dict, List, Tuple 10 | 11 | import numpy as np 12 | 13 | from . import logger 14 | from .json_loader import get_json_files, load_json_file 15 | 16 | 17 | __all__ = [ 18 | "EmotionType", 19 | "EmotionEvent", 20 | 21 | "load_emotion_types", 22 | "load_emotion_events", 23 | ] 24 | 25 | 26 | class Node: 27 | def __init__(self, x: float, y: float): 28 | self.x = float(x) 29 | self.y = float(y) 30 | 31 | 32 | class DecayGraph: 33 | __slots__ = [ 34 | "nodes_x", 35 | "nodes_y", 36 | "ext_line_params", 37 | ] 38 | 39 | def __init__(self, nodes: List[Node]) -> None: 40 | self.nodes_x = [node.x for node in nodes] 41 | self.nodes_y = [node.y for node in nodes] 42 | self.ext_line_params = self.get_line_parameters(nodes[-2], nodes[-1]) if len(nodes) > 1 else None 43 | 44 | def get_increment(self, val) -> float: 45 | if self.ext_line_params is None: 46 | f_out = self.nodes_y[0] 47 | elif val <= self.nodes_x[-1]: 48 | f_out = np.interp(val, self.nodes_x, self.nodes_y) 49 | else: 50 | f_out = self.ext_line_params[0] * val + self.ext_line_params[1] 51 | return f_out 52 | 53 | @staticmethod 54 | def get_line_parameters(p1: Node, p2: Node) -> Tuple[float]: 55 | try: 56 | m = (p1.y - p2.y) / (p1.x - p2.x) 57 | b = p1.y - m * p1.x 58 | except ZeroDivisionError: 59 | m, b = 0, p1.y 60 | return m, b 61 | 62 | 63 | class EmotionType: 64 | """ Emotion type class. """ 65 | 66 | __slots__ = [ 67 | "name", 68 | "decay_graph", 69 | "repetition_penalty" 70 | ] 71 | 72 | def __init__(self, name: str, decay_graph: DecayGraph, repetition_penaly: DecayGraph) -> None: 73 | self.name = str(name) 74 | self.decay_graph = decay_graph 75 | self.repetition_penalty = repetition_penaly 76 | 77 | def update(self): 78 | """ Update from decay function. """ 79 | # TODO 80 | pass 81 | 82 | 83 | class EmotionEvent: 84 | """ EmotionEvent representation class. """ 85 | 86 | __slots__ = [ 87 | "name", 88 | "affectors", 89 | ] 90 | 91 | def __init__(self, name: str, affectors: Dict[str, float]) -> None: 92 | self.name = str(name) 93 | self.affectors = dict(affectors) 94 | 95 | @classmethod 96 | def from_json(cls, data: Dict): 97 | affectors = {} 98 | for affector in data['emotionAffectors']: 99 | affectors[affector['emotionType']] = affector['value'] 100 | return cls(name=data['name'], affectors=affectors) 101 | 102 | 103 | def load_emotion_types(resource_dir: str) -> Dict[str, EmotionType]: 104 | 105 | start_time = time.perf_counter() 106 | 107 | json_data = load_json_file( 108 | os.path.join(resource_dir, 'cozmo_resources', 'config', 'engine', 'mood_config.json')) 109 | 110 | decay_graphs = {} 111 | for graph in json_data['decayGraphs']: 112 | nodes = [Node(x=n['x'], y=n['y']) for n in graph['nodes']] 113 | decay_graphs[graph['emotionType']] = DecayGraph(nodes) 114 | 115 | # Note: the repetition penalty might be linked not only to emotion events but also any activities or behaviors. 116 | default_rp = DecayGraph([Node(x=n['x'], y=n['y']) for n in json_data['defaultRepetitionPenalty']['nodes']]) 117 | 118 | emotion_types = { 119 | "WantToPlay": EmotionType("WantToPlay", decay_graphs.get('WantToPlay', decay_graphs['default']), default_rp), 120 | "Social": EmotionType("Social", decay_graphs.get('Social', decay_graphs['default']), default_rp), 121 | "Confident": EmotionType("Confident", decay_graphs.get('Confident', decay_graphs['default']), default_rp), 122 | "Excited": EmotionType("Excited", decay_graphs.get('Excited', decay_graphs['default']), default_rp), 123 | "Happy": EmotionType("Happy", decay_graphs.get('Happy', decay_graphs['default']), default_rp), 124 | "Calm": EmotionType("Calm", decay_graphs.get('Calm', decay_graphs['default']), default_rp), 125 | "Brave": EmotionType("Brave", decay_graphs.get('Brave', decay_graphs['default']), default_rp), 126 | } 127 | 128 | logger.debug("Loaded emotion types in {:.02f} s.".format(time.perf_counter() - start_time)) 129 | 130 | return emotion_types 131 | 132 | 133 | def load_emotion_events(resource_dir: str) -> Dict[str, EmotionEvent]: 134 | 135 | start_time = time.perf_counter() 136 | 137 | emotion_files = get_json_files(resource_dir, 138 | [os.path.join('cozmo_resources', 'config', 'engine', 'emotionevents/')]) 139 | emotion_events = {} 140 | 141 | for ef in emotion_files: 142 | json_data = load_json_file(ef) 143 | if 'emotionEvents' not in json_data: 144 | emotion_events[json_data['name']] = EmotionEvent.from_json(json_data) 145 | else: 146 | for event in json_data['emotionEvents']: 147 | emotion_events[event['name']] = EmotionEvent.from_json(event) 148 | 149 | logger.debug("Loaded {} emotion events in {:.02f} s.".format( 150 | len(emotion_events), time.perf_counter() - start_time)) 151 | 152 | return emotion_events 153 | -------------------------------------------------------------------------------- /tools/pycozmo_update.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | 4 | Cozmo robot over-the-air (OTA) firmware update application. 5 | 6 | """ 7 | 8 | from typing import Optional 9 | import sys 10 | import os 11 | import json 12 | import math 13 | from threading import Event 14 | import time 15 | import argparse 16 | 17 | import pycozmo 18 | 19 | 20 | safe_file = "" 21 | verbose = False 22 | 23 | chunk_id = 0 24 | evt = Event() 25 | 26 | last_chunk_id = -1 27 | last_status = -1 28 | 29 | 30 | class UpdateError(Exception): 31 | """ Update exception. """ 32 | pass 33 | 34 | 35 | def on_firmware_update_result(_, pkt: pycozmo.protocol_encoder.FirmwareUpdateResult) -> None: 36 | """ FirmwareUpdateResult packet handler. """ 37 | 38 | global last_chunk_id 39 | global last_status 40 | 41 | last_chunk_id = pkt.chunk_id 42 | last_status = pkt.status 43 | 44 | if verbose: 45 | print("Progress: {} bytes / {} chunks; status {}".format(pkt.byte_count, last_chunk_id, last_status)) 46 | 47 | evt.set() 48 | 49 | 50 | def wait_for_result(timeout: Optional[float] = None) -> None: 51 | """ Wait for result message. """ 52 | 53 | evt.wait(timeout=timeout) 54 | evt.clear() 55 | 56 | # 0 indicates operation success and 10 indicates update completion. 57 | if last_status and last_status != 10: 58 | raise UpdateError("Update failed with error code {}.".format(last_status)) 59 | 60 | 61 | def send_chunk(cli: pycozmo.client.Client, f) -> True: 62 | """ Read and send a chunk. """ 63 | 64 | global chunk_id 65 | 66 | chunk = f.read(1024) 67 | if chunk: 68 | if len(chunk) < 1024: 69 | # Pad last chunk. 70 | chunk = chunk.ljust(1024, b"\0") 71 | res = True 72 | else: 73 | res = False 74 | 75 | pkt = pycozmo.protocol_encoder.FirmwareUpdate(chunk_id=chunk_id, data=chunk) 76 | cli.conn.send(pkt) 77 | 78 | chunk_id += 1 79 | else: 80 | # Reached end of file. 81 | res = True 82 | 83 | return res 84 | 85 | 86 | def update(cli: pycozmo.client.Client) -> None: 87 | """ Perform robot OTA firmware update. """ 88 | 89 | # Register for FirmwareUpdateResult packets. 90 | cli.add_handler(pycozmo.protocol_encoder.FirmwareUpdateResult, on_firmware_update_result) 91 | 92 | safe_size = os.path.getsize(safe_file) 93 | total_chunks = math.ceil(safe_size / 1024) 94 | if verbose: 95 | print("Safe size: {} bytes / {} chunks".format(safe_size, total_chunks)) 96 | 97 | with open(safe_file, "rb") as f: 98 | 99 | print("Initiating update...") 100 | send_chunk(cli, f) 101 | wait_for_result(30.0) 102 | if last_status != 0: 103 | raise UpdateError("Failed to receive initialization confirmation.") 104 | 105 | print("Transferring firmware...") 106 | done = False 107 | while not done: 108 | if verbose: 109 | print("Sending chunk {}/{}...".format(chunk_id+1, total_chunks)) 110 | # For some reason the robot sends FirmwareUpdateResult only every 2 chunks. 111 | send_chunk(cli, f) 112 | done = send_chunk(cli, f) 113 | wait_for_result(0.5) 114 | 115 | # Finalize update 116 | print("Finalizing update...") 117 | pkt = pycozmo.protocol_encoder.FirmwareUpdate(chunk_id=0xFFFF, data=b"\0" * 1024) 118 | cli.conn.send(pkt) 119 | wait_for_result(10.0) 120 | if last_status != 10: 121 | raise UpdateError("Failed to receive update confirmation (status {}).".format(last_status)) 122 | 123 | time.sleep(15.0) 124 | 125 | print("Done.") 126 | 127 | 128 | def verify_safe(fspec: str, signature: bool) -> None: 129 | """ Perform .safe file sanity check. """ 130 | 131 | try: 132 | with open(fspec, "rb") as f: 133 | raw_sig = f.read(1024).decode("utf-8").rstrip("\0") 134 | sig = json.loads(raw_sig) 135 | except Exception as e: 136 | print("ERROR: Failed to read signature from .safe file. {}".format(e)) 137 | sys.exit(1) 138 | 139 | for k in ("version", "wifiSig", "rtipSig", "bodySig"): 140 | if k not in sig: 141 | print("ERROR: Invalid .safe file.") 142 | sys.exit(2) 143 | 144 | if signature: 145 | print(json.dumps(sig, indent=4, separators=(",", ": "))) 146 | sys.exit(0) 147 | else: 148 | print("Updating to version {}...".format(sig["version"])) 149 | 150 | 151 | def parse_args(): 152 | """ Parse command-line arguments. """ 153 | parser = argparse.ArgumentParser(description=__doc__) 154 | parser.add_argument("-v", "--verbose", action="store_true", help="verbose") 155 | parser.add_argument("-s", "--signature", action="store_true", help="Dump .safe signature and exit") 156 | parser.add_argument("safe_file", help=".safe file specification") 157 | args = parser.parse_args() 158 | return args 159 | 160 | 161 | def main(): 162 | global safe_file 163 | global verbose 164 | 165 | # Parse command-line. 166 | args = parse_args() 167 | 168 | safe_file = args.safe_file 169 | verbose = args.verbose 170 | 171 | # Verify .safe file. 172 | verify_safe(safe_file, args.signature) 173 | 174 | # Update robot. 175 | try: 176 | with pycozmo.connect(protocol_log_level="INFO", robot_log_level="DEBUG", auto_initialize=False) as cli: 177 | update(cli) 178 | except Exception as e: 179 | print("ERROR: {}".format(e)) 180 | sys.exit(3) 181 | 182 | 183 | if __name__ == '__main__': 184 | main() 185 | -------------------------------------------------------------------------------- /pycozmo/behavior.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | Behavior representation and reading. 4 | 5 | """ 6 | 7 | import os 8 | import time 9 | from typing import Dict, Optional, Any 10 | 11 | from . import event 12 | from . import client 13 | from . import logger 14 | from .json_loader import get_json_files, load_json_file 15 | 16 | 17 | __all__ = [ 18 | "ReactionTrigger", 19 | "Behavior", 20 | 21 | "load_behaviors", 22 | "load_reaction_trigger_behavior_map", 23 | ] 24 | 25 | 26 | class ReactionTrigger: 27 | """ Reaction trigger representation class. """ 28 | __slots__ = [ 29 | "name", 30 | "behavior_id", 31 | "should_resume_last", 32 | ] 33 | 34 | def __init__(self, name: str, behavior_id: str, should_resume_last: Optional[bool] = False): 35 | self.name = str(name) 36 | self.behavior_id = str(behavior_id) 37 | self.should_resume_last = bool(should_resume_last) 38 | 39 | @classmethod 40 | def from_json(cls, data: Dict): 41 | return cls(name=data['reactionTrigger'], 42 | behavior_id=data['behaviorID'], 43 | should_resume_last=data.get('genericStrategyParams', {}).get('shouldResumeLast')) 44 | 45 | 46 | class Behavior(event.Dispatcher): 47 | """ Behavior representation class. """ 48 | 49 | def __init__(self, cli: client.Client, conf: Any) -> None: 50 | super().__init__() 51 | self.cli = cli 52 | self.conf = conf 53 | 54 | def get_id(self) -> str: 55 | return self.conf["behaviorID"] 56 | 57 | def activate(self) -> None: 58 | logger.warning("Behavior '{}' not implemented.".format(self.get_id())) 59 | self.cli.conn.post_event(event.EvtBehaviorDone, self.cli) 60 | 61 | def deactivate(self) -> None: 62 | pass 63 | 64 | 65 | class BehaviorPlayAnim(Behavior): 66 | """ Play a sequence of animation triggers. """ 67 | 68 | def __init__(self, cli: client.Client, conf: Any): 69 | super().__init__(cli, conf) 70 | self.anim_triggers = conf.get("animTriggers", []) 71 | self.current_trigger = 0 72 | self.add_handler(event.EvtAnimationCompleted, self._on_animation_completed) 73 | 74 | def activate(self) -> None: 75 | self.current_trigger = 0 76 | if self.anim_triggers: 77 | anim_trigger = self.anim_triggers[self.current_trigger] 78 | self.current_trigger += 1 79 | self.cli.play_anim_group(anim_trigger) 80 | 81 | def _on_animation_completed(self, cli): 82 | # TODO: Support additional animation triggers. 83 | self.cli.conn.post_event(event.EvtBehaviorDone, self.cli) 84 | 85 | def deactivate(self) -> None: 86 | self.cli.cancel_anim() 87 | 88 | 89 | class BehaviorPlayArbitraryAnim(BehaviorPlayAnim): 90 | """ Play a random animation trigger. """ 91 | 92 | def activate(self) -> None: 93 | # TODO: Pick random animation trigger and put it in sequence 94 | super().activate() 95 | 96 | 97 | class BehaviorReactToCliff(BehaviorPlayAnim): 98 | """ ReactToCliff behavior - currently, just plays animation. """ 99 | 100 | def activate(self) -> None: 101 | self.anim_triggers = ("ReactToCliff", ) 102 | super().activate() 103 | 104 | 105 | class BehaviorDriveOffCharger(Behavior): 106 | 107 | def activate(self) -> None: 108 | # extraDistanceToDrive_mm = float(self.conf["extraDistanceToDrive_mm"]) 109 | # TODO: Play wake up animation? 110 | # TODO: Drive and wait for completion. 111 | self.cli.conn.post_event(event.EvtBehaviorDone, self.cli) 112 | 113 | def deactivate(self) -> None: 114 | # TODO: Cancel 115 | pass 116 | 117 | 118 | def get_behavior_class_from_dict(data): 119 | """ Choose a behavior class, based on the behaviorClass JSON attribute. """ 120 | # TODO: Replace with a behavior package. 121 | class_map = { 122 | "PlayAnim": BehaviorPlayAnim, 123 | "PlayArbitraryAnim": BehaviorPlayArbitraryAnim, 124 | "ReactToCliff": BehaviorReactToCliff, 125 | "DriveOffCharger": BehaviorDriveOffCharger, 126 | } 127 | cls = class_map.get(data["behaviorClass"], Behavior) 128 | return cls 129 | 130 | 131 | def load_behaviors(resource_dir: str, cli: client.Client) -> Dict[str, Behavior]: 132 | 133 | start_time = time.perf_counter() 134 | 135 | behavior_files = get_json_files( 136 | resource_dir, [os.path.join('cozmo_resources', 'config', 'engine', 'behaviorSystem', 'behaviors')]) 137 | behaviors = {} 138 | for filename in behavior_files: 139 | data = load_json_file(filename) 140 | cls = get_behavior_class_from_dict(data) 141 | behaviors[data['behaviorID']] = cls(cli, data) 142 | 143 | logger.debug("Loaded {} behaviors in {:.02f} s.".format(len(behaviors), time.perf_counter() - start_time)) 144 | 145 | return behaviors 146 | 147 | 148 | def load_reaction_trigger_behavior_map(resource_dir: str) -> Dict[str, ReactionTrigger]: 149 | 150 | start_time = time.perf_counter() 151 | 152 | reaction_trigger_behavior_map = {} 153 | filename = os.path.join(resource_dir, 'cozmo_resources', 'config', 154 | 'engine', 'behaviorSystem', 'reactionTrigger_behavior_map.json') 155 | 156 | json_data = load_json_file(filename) 157 | for trigger in json_data['reactionTriggerBehaviorMap']: 158 | reaction_trigger_behavior_map[trigger['reactionTrigger']] = ReactionTrigger.from_json(trigger) 159 | 160 | logger.debug("Loaded {} entry reaction trigger behavior map in {:.02f} s.".format( 161 | len(reaction_trigger_behavior_map), time.perf_counter() - start_time)) 162 | 163 | return reaction_trigger_behavior_map 164 | -------------------------------------------------------------------------------- /docs/versions.md: -------------------------------------------------------------------------------- 1 | Cozmo Firmware Versions 2 | ======================= 3 | 4 | Cozmo firmware images can be found under `com.anki.cozmo/files/cozmo/cozmo_resources/config/engine/firmware` in the 5 | Cozmo app. 6 | 7 | 8 | Production Versions 9 | ------------------- 10 | 11 | ```json 12 | { 13 | "version": 2381, 14 | "git-rev": "408d28a7f6e68cbb5b29c1dcd8c8db2b38f9c8ce", 15 | "date": "Tue Jan 8 10:27:05 2019", 16 | "time": 1546972025, 17 | "messageEngineToRobotHash": "9e4a965ace4e09d86997b87ba14235d5", 18 | "messageRobotToEngineHash": "a259247f16231db440957215baba12ab", 19 | "build": "DEVELOPMENT", 20 | "wifiSig": "69ca03352e42143d340f0f7fac02ed8ff96ef10b", 21 | "rtipSig": "36574986d76144a70e9252ab633be4617a4bc661", 22 | "bodySig": "695b59eff43664acd1a5a956d08c682b3f8bd2c8" 23 | } 24 | ``` 25 | 26 | ```json 27 | { 28 | "version": 2380, 29 | "git-rev": "6ef227df0d64427f95cb943e01d8ac3956646e4d", 30 | "date": "Thu Dec 20 17:33:45 2018", 31 | "time": 1545356025, 32 | "messageEngineToRobotHash": "3aed3b94dbf19e11b2775ff980874213", 33 | "messageRobotToEngineHash": "c5a95cb6f44c1b89a42784d0c637fda8", 34 | "build": "DEVELOPMENT", 35 | "wifiSig": "8694122d7de45ee085c488274d28b69b7b1f2f44", 36 | "rtipSig": "8acba259c7b440dc0a3467ae73f262a224f036db", 37 | "bodySig": "14d4420c42432211ae4cda4f78a41841b03a6b40" 38 | } 39 | ``` 40 | 41 | ```json 42 | { 43 | "version": 2315, 44 | "git-rev": "d96caf034da1c4a33d70d2c1e3bc5732ec68594a", 45 | "date": "Thu Nov 9 15:37:45 2017", 46 | "time": 1510270665, 47 | "messageEngineToRobotHash": "5d963ecd52d4ae18af796f14f02a3f60", 48 | "messageRobotToEngineHash": "d07d1f4dea884725adefd33de221a49f", 49 | "build": "DEVELOPMENT", 50 | "wifiSig": "2749d9fb97a138aa7b56429c3a587baf6dadfb6f", 51 | "rtipSig": "0605ff5cd1f37cf75573caac3678ecba12b9bebe", 52 | "bodySig": "76dc76aa624fac230603101206d3a4e2e50e76cb" 53 | } 54 | ``` 55 | 56 | ```json 57 | { 58 | "version": 2313, 59 | "git-rev": "7381fe56705992ffd03bef1bb1a7b2e7258e9bc2", 60 | "date": "Tue Nov 7 21:13:04 2017", 61 | "time": 1510117984, 62 | "messageEngineToRobotHash": "838bbe94628fd10783e40f6b6b9874df", 63 | "messageRobotToEngineHash": "6ae9b7733e469f4fef89479d63e214ba", 64 | "build": "DEVELOPMENT", 65 | "wifiSig": "5bfbabc73e0ec5e20a072b6ab87b60da8a51310a", 66 | "rtipSig": "349d2224cc00e56ee50a5b4ecb905a5ba64c791d", 67 | "bodySig": "5ac6821655294e88b5fb852427bd99120af16fb5" 68 | } 69 | ``` 70 | 71 | ```json 72 | { 73 | "version": 2214, 74 | "git-rev": "c363ccc897bc3748d234f80c21e4c8a33757d063", 75 | "date": "Wed Aug 9 11:01:32 2017", 76 | "time": 1502301692, 77 | "messageEngineToRobotHash": "861bbc71828456c0f073c4464bdcb21e", 78 | "messageRobotToEngineHash": "2dc8419f768f6f3fd4843cbb0a86f7f7", 79 | "build": "DEVELOPMENT", 80 | "wifiSig": "da7eb444c13475eb67b0c13336b24021b8cf540f", 81 | "rtipSig": "4cba42517073e77967ce8c7340376713001b4d0a", 82 | "bodySig": "74a1776d1c6a4213ccfbb0ad2c4099eafdf7ad0c" 83 | } 84 | ``` 85 | 86 | ```json 87 | { 88 | "version": 2158, 89 | "git-rev": "44c8d8a1d3a2b09b54da0ff4b6ceee75ec66e267", 90 | "date": "Thu Jun 15 10:00:23 2017", 91 | "time": 1497546023, 92 | "messageEngineToRobotHash": "71beec8d11144f3a3718d2cc5ea602f3", 93 | "messageRobotToEngineHash": "4018b2e764ec08f5fcacdb6358847cb0", 94 | "build": "DEVELOPMENT", 95 | "wifiSig": "e3f4a7e29b76321e3563f50e0b09c89378b5dc97", 96 | "rtipSig": "64efe94218e8eaac3576f2405bc5f01f020b0b7a", 97 | "bodySig": "d0c34ed006c71abe45ac735e4bb68bf1153b082b" 98 | } 99 | ``` 100 | 101 | ```json 102 | { 103 | "version": 1889, 104 | "git-rev": "e541e4247376d7945fd42a82a826b443effbeff2", 105 | "date": "Thu Mar 23 17:15:50 2017", 106 | "time": 1490314550, 107 | "messageEngineToRobotHash": "7098b4a266c0ccc2102a61fda53b8999", 108 | "messageRobotToEngineHash": "9b83f21da9120fdeebfeabe84af81c32", 109 | "build": "DEVELOPMENT", 110 | "wifiSig": "266d1d4f91c5ee069e628550a0331e8b0eb90f2b", 111 | "rtipSig": "bc90e2949be66851fb7ac035f5de9b52ff69fd14", 112 | "bodySig": "ccbb209db374f21ef233945f1515a70b8fe43114" 113 | } 114 | ``` 115 | 116 | ```json 117 | { 118 | "version": 1859, 119 | "git-rev": "11a52d6a4f2c5d89cef7085b836e8d0f2525808b", 120 | "date": "Mon Mar 20 23:29:56 2017", 121 | "time": 1490077796, 122 | "messageEngineToRobotHash": "54195812be0de998a4ebde795364d62b", 123 | "messageRobotToEngineHash": "90d8f3273055624b8444fbcbef555ee8", 124 | "build": "DEVELOPMENT", 125 | "wifiSig": "79dca08e85f21311e5551e38ecf0d3dab6ce006f", 126 | "rtipSig": "72519cd2bfb11bc799915dd8506a67b0ae5186da", 127 | "bodySig": "8746362ebc89e6235e3da103b9e9c0133cc3d1c1" 128 | } 129 | ``` 130 | 131 | ```json 132 | { 133 | "version": 1299, 134 | "git-rev": "6ced81297ac14067662acbed79cecac7f5eacd28", 135 | "date": "Mon Nov 21 15:25:58 2016", 136 | "time": 1479770758, 137 | "messageEngineToRobotHash": "61879d8808f0308cd8ae6340ddfe06e6", 138 | "messageRobotToEngineHash": "5914fda0b97c7aadaf0e4d97fc72610f", 139 | "wifiSig": "6cd4d9263e7a5b5da9eedc33e32c8baeb04a0ea6", 140 | "rtipSig": "24591dd715955eef0c1c7f0d89b4b41c122cbb26", 141 | "bodySig": "412ce6fc22f7407cb2e87eaacee3e9c4d7ca47ea" 142 | } 143 | ``` 144 | 145 | 146 | Factory Versions 147 | ---------------- 148 | 149 | Cozmo factory firmwares identify with large version numbers. 150 | 151 | Seen on Cozmo with HW v1.5: 152 | 153 | ```json 154 | { 155 | "build": "FACTORY", 156 | "version": 10501, 157 | "date": "Fri Apr 14 20:28:21 2017", 158 | "time": 1492201868 159 | } 160 | ``` 161 | 162 | ```json 163 | { 164 | "build": "FACTORY", 165 | "version": 10502, 166 | "date": "Mon Aug 7 09:21:24 2017", 167 | "time": 1502122884 168 | } 169 | ``` 170 | 171 | Seen on development Cozmo with HW v1.7: 172 | 173 | ```json 174 | { 175 | "build": "FACTORY", 176 | "version": 10700, 177 | "date": "Thu Mar 28 14:18:13 2019", 178 | "time": 1553807893 179 | } 180 | ``` 181 | -------------------------------------------------------------------------------- /pycozmo/robot.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | Robot constants and helper code. 4 | 5 | """ 6 | 7 | import enum 8 | import math 9 | 10 | from . import util 11 | 12 | 13 | __all__ = [ 14 | "MIN_HEAD_ANGLE", 15 | "MAX_HEAD_ANGLE", 16 | "MIN_LIFT_HEIGHT", 17 | "MAX_LIFT_HEIGHT", 18 | "LIFT_ARM_LENGTH", 19 | "LIFT_PIVOT_HEIGHT", 20 | "MIN_LIFT_ANGLE", 21 | "MAX_LIFT_ANGLE", 22 | "MAX_WHEEL_SPEED", 23 | "TRACK_WIDTH", 24 | "FRAME_RATE", 25 | 26 | "RobotStatusFlag", 27 | "RobotStatusFlagNames", 28 | 29 | "LiftPosition", 30 | ] 31 | 32 | 33 | #: Minimum head angle. 34 | MIN_HEAD_ANGLE = util.Angle(degrees=-25) 35 | #: Maximum head angle. 36 | MAX_HEAD_ANGLE = util.Angle(degrees=44.5) 37 | 38 | #: Minimum lift height. 39 | MIN_LIFT_HEIGHT = util.Distance(mm=32.0) 40 | #: Maximum lift height. 41 | MAX_LIFT_HEIGHT = util.Distance(mm=92.0) 42 | 43 | #: Lift arm length. 44 | LIFT_ARM_LENGTH = util.Distance(mm=66.0) 45 | #: Lift arm pivot point height. 46 | LIFT_PIVOT_HEIGHT = util.Distance(mm=45.0) 47 | 48 | #: Minimum lift arm angle. 49 | MIN_LIFT_ANGLE = util.Angle(radians=math.asin((MIN_LIFT_HEIGHT.mm - LIFT_PIVOT_HEIGHT.mm) / LIFT_ARM_LENGTH.mm)) 50 | #: Maximum lift arm angle. 51 | MAX_LIFT_ANGLE = util.Angle(radians=math.asin((MAX_LIFT_HEIGHT.mm - LIFT_PIVOT_HEIGHT.mm) / LIFT_ARM_LENGTH.mm)) 52 | 53 | #: Maximum wheel speed. 54 | MAX_WHEEL_SPEED = util.Speed(mmps=200.0) 55 | 56 | #: Track width. 57 | TRACK_WIDTH = util.Distance(mm=45.0) 58 | 59 | #: Number of frames per second for animations. 60 | FRAME_RATE = 30 61 | 62 | 63 | class RobotStatusFlag(object): 64 | # Robot status flags. 65 | IS_MOVING = 0x1 66 | IS_CARRYING_BLOCK = 0x2 67 | IS_PICKING_OR_PLACING = 0x4 68 | IS_PICKED_UP = 0x8 69 | IS_BODY_ACC_MODE = 0x10 70 | IS_FALLING = 0x20 71 | IS_ANIMATING = 0x40 72 | IS_PATHING = 0x80 73 | LIFT_IN_POS = 0x100 74 | HEAD_IN_POS = 0x200 75 | IS_ANIM_BUFFER_FULL = 0x400 76 | IS_ANIMATING_IDLE = 0x800 77 | IS_ON_CHARGER = 0x1000 78 | IS_CHARGING = 0x2000 79 | CLIFF_DETECTED = 0x4000 80 | ARE_WHEELS_MOVING = 0x8000 81 | IS_CHARGER_OOS = 0x10000 82 | 83 | 84 | #: Robot status flag names. 85 | RobotStatusFlagNames = { 86 | RobotStatusFlag.IS_MOVING: "IS_MOVING", 87 | RobotStatusFlag.IS_CARRYING_BLOCK: "IS_CARRYING_BLOCK", 88 | RobotStatusFlag.IS_PICKING_OR_PLACING: "IS_PICKING_OR_PLACING", 89 | RobotStatusFlag.IS_PICKED_UP: "IS_PICKED_UP", 90 | RobotStatusFlag.IS_BODY_ACC_MODE: "IS_BODY_ACC_MODE", 91 | RobotStatusFlag.IS_FALLING: "IS_FALLING", 92 | RobotStatusFlag.IS_ANIMATING: "IS_ANIMATING", 93 | RobotStatusFlag.IS_PATHING: "IS_PATHING", 94 | RobotStatusFlag.LIFT_IN_POS: "LIFT_IN_POS", 95 | RobotStatusFlag.HEAD_IN_POS: "HEAD_IN_POS", 96 | RobotStatusFlag.IS_ANIM_BUFFER_FULL: "IS_ANIM_BUFFER_FULL", 97 | RobotStatusFlag.IS_ANIMATING_IDLE: "IS_ANIMATING_IDLE", 98 | RobotStatusFlag.IS_ON_CHARGER: "IS_ON_CHARGER", 99 | RobotStatusFlag.IS_CHARGING: "IS_CHARGING", 100 | RobotStatusFlag.CLIFF_DETECTED: "CLIFF_DETECTED", 101 | RobotStatusFlag.ARE_WHEELS_MOVING: "ARE_WHEELS_MOVING", 102 | RobotStatusFlag.IS_CHARGER_OOS: "IS_CHARGER_OOS", 103 | } 104 | 105 | 106 | class RobotOrientation(enum.Enum): 107 | """ Robot orientation enumeration. """ 108 | ON_THREADS = 0 109 | ON_BACK = 1 110 | ON_FACE = 2 111 | ON_LEFT_SIDE = 3 112 | ON_RIGHT_SIDE = 4 113 | 114 | 115 | class LiftPosition(object): 116 | """ 117 | Represents the position of Cozmo's lift. 118 | 119 | The class allows the position to be referred to as either absolute height 120 | above the ground, as a ratio from 0.0 to 1.0, or as the angle of the lift 121 | arm relative to the ground. 122 | 123 | Args: 124 | height (:class:`cozmo.util.Distance`): The height of the lift above the ground. 125 | ratio (float): The ratio from 0.0 to 1.0 that the lift is raised from the ground. 126 | angle (:class:`cozmo.util.Angle`): The angle of the lift arm relative to the ground. 127 | """ 128 | 129 | __slots__ = ('_height', ) 130 | 131 | def __init__(self, height=None, ratio=None, angle=None): 132 | def _count_arg(arg): 133 | # return 1 if argument is set (not None), 0 otherwise 134 | return 0 if (arg is None) else 1 135 | num_provided_args = _count_arg(height) + _count_arg(ratio) + _count_arg(angle) 136 | if num_provided_args != 1: 137 | raise ValueError("Expected one, and only one, of the distance, ratio or angle keyword arguments") 138 | 139 | if height is not None: 140 | if not isinstance(height, util.Distance): 141 | raise TypeError("Unsupported type for distance - expected util.Distance") 142 | self._height = height 143 | elif ratio is not None: 144 | height_mm = MIN_LIFT_HEIGHT.mm + (ratio * (MAX_LIFT_HEIGHT.mm - MIN_LIFT_HEIGHT.mm)) 145 | self._height = util.Distance(mm=height_mm) 146 | elif angle is not None: 147 | if not isinstance(angle, util.Angle): 148 | raise TypeError("Unsupported type for angle - expected util.Angle") 149 | height_mm = (math.sin(angle.radians) * LIFT_ARM_LENGTH.mm) + LIFT_PIVOT_HEIGHT.mm 150 | self._height = util.Distance(mm=height_mm) 151 | 152 | def __repr__(self): 153 | return "<%s height=%s ratio=%s angle=%s>" % (self.__class__.__name__, self._height, self.ratio, self.angle) 154 | 155 | @property 156 | def height(self) -> util.Distance: 157 | """ Height above the ground. """ 158 | return self._height 159 | 160 | @property 161 | def ratio(self) -> float: 162 | """ The ratio from 0 to 1 that the lift is raised, 0 at the bottom, 1 at the top. """ 163 | ratio = ((self._height.mm - MIN_LIFT_HEIGHT.mm) / (MAX_LIFT_HEIGHT.mm - MIN_LIFT_HEIGHT.mm)) 164 | return ratio 165 | 166 | @property 167 | def angle(self) -> util.Angle: 168 | """ The angle of the lift arm relative to the ground. """ 169 | sin_angle = (self._height.mm - LIFT_PIVOT_HEIGHT.mm) / LIFT_ARM_LENGTH.mm 170 | angle = math.asin(sin_angle) 171 | return util.Angle(radians=angle) 172 | -------------------------------------------------------------------------------- /pycozmo/audiokinetic/tests/test_soundbanksinfo.py: -------------------------------------------------------------------------------- 1 | 2 | import unittest 3 | import io 4 | 5 | import pycozmo 6 | 7 | 8 | class TestLoadSoundbanksInfo(unittest.TestCase): 9 | 10 | def test_invalid(self): 11 | f = io.StringIO("") 12 | with self.assertRaises(pycozmo.audiokinetic.exception.AudioKineticFormatError): 13 | pycozmo.audiokinetic.soundbanksinfo.load_soundbanksinfo(f) 14 | 15 | def test_empty(self): 16 | dump = r""" 17 | 18 | 19 | 20 | 21 | """ 22 | f = io.StringIO(dump) 23 | objs = pycozmo.audiokinetic.soundbanksinfo.load_soundbanksinfo(f) 24 | self.assertEqual(len(objs), 0) 25 | 26 | def test_soundbankinfo(self): 27 | dump = r""" 28 | 29 | 30 | 31 | \SoundBanks\Default Work Unit\SFX 32 | SFX 33 | SFX.bnk 34 | 35 | 36 | 37 | """ 38 | f = io.StringIO(dump) 39 | objs = pycozmo.audiokinetic.soundbanksinfo.load_soundbanksinfo(f) 40 | self.assertEqual(len(objs), 1) 41 | soundbank = objs[393239870] 42 | self.assertIsInstance(soundbank, pycozmo.audiokinetic.soundbanksinfo.SoundBankInfo) 43 | self.assertEqual(soundbank.id, 393239870) 44 | self.assertEqual(soundbank.name, "SFX") 45 | self.assertEqual(soundbank.path, "SFX.bnk") 46 | self.assertEqual(soundbank.language, "SFX") 47 | self.assertEqual(soundbank.object_path, r"\SoundBanks\Default Work Unit\SFX") 48 | 49 | def test_eventinfo(self): 50 | dump = r""" 51 | 52 | 53 | 54 | \SoundBanks\Default Work Unit\SFX 55 | SFX 56 | SFX.bnk 57 | 58 | 60 | 61 | 62 | 63 | 64 | """ 65 | f = io.StringIO(dump) 66 | objs = pycozmo.audiokinetic.soundbanksinfo.load_soundbanksinfo(f) 67 | self.assertEqual(len(objs), 2) 68 | event = objs[72626837] 69 | self.assertIsInstance(event, pycozmo.audiokinetic.soundbanksinfo.EventInfo) 70 | self.assertEqual(event.soundbank_id, 393239870) 71 | self.assertEqual(event.id, 72626837) 72 | self.assertEqual(event.name, "Play__Codelab__SFX_Alien_Invasion_UFO") 73 | self.assertEqual(event.object_path, r"\Events\Gameplay_SFX\Codelab__SFX\Play__Codelab__SFX_Alien_Invasion_UFO") 74 | 75 | def test_fileinfo_embedded(self): 76 | dump = r""" 77 | 78 | 79 | 80 | \SoundBanks\Default Work Unit\SFX 81 | SFX 82 | SFX.bnk 83 | 84 | 85 | Codelab__SFX_General_Negative.wav 86 | SFX\Codelab__SFX_General_Negative_4B76E3B5.wem 87 | 4084 88 | 89 | 90 | 91 | 92 | 93 | """ 94 | f = io.StringIO(dump) 95 | objs = pycozmo.audiokinetic.soundbanksinfo.load_soundbanksinfo(f) 96 | self.assertEqual(len(objs), 2) 97 | file = objs[26755609] 98 | self.assertIsInstance(file, pycozmo.audiokinetic.soundbanksinfo.FileInfo) 99 | self.assertEqual(file.soundbank_id, 393239870) 100 | self.assertEqual(file.id, 26755609) 101 | self.assertEqual(file.name, "Codelab__SFX_General_Negative.wav") 102 | self.assertEqual(file.path, r"SFX\Codelab__SFX_General_Negative_4B76E3B5.wem") 103 | self.assertEqual(file.embedded, True) 104 | self.assertEqual(file.prefetch_size, 4084) 105 | 106 | def test_fileinfo_streamed(self): 107 | dump = r""" 108 | 109 | 110 | 111 | Codelab__SFX_General_Negative.wav 112 | SFX\Codelab__SFX_General_Negative_4B76E3B5.wem 113 | 114 | 115 | 116 | 117 | \SoundBanks\Default Work Unit\SFX 118 | SFX 119 | SFX.bnk 120 | 121 | 122 | 123 | 124 | 125 | 126 | """ 127 | f = io.StringIO(dump) 128 | objs = pycozmo.audiokinetic.soundbanksinfo.load_soundbanksinfo(f) 129 | self.assertEqual(len(objs), 2) 130 | file = objs[26755609] 131 | self.assertIsInstance(file, pycozmo.audiokinetic.soundbanksinfo.FileInfo) 132 | self.assertEqual(file.soundbank_id, 393239870) 133 | self.assertEqual(file.id, 26755609) 134 | self.assertEqual(file.name, "Codelab__SFX_General_Negative.wav") 135 | self.assertEqual(file.path, r"SFX\Codelab__SFX_General_Negative_4B76E3B5.wem") 136 | self.assertEqual(file.embedded, False) 137 | self.assertEqual(file.prefetch_size, -1) 138 | -------------------------------------------------------------------------------- /pycozmo/audiokinetic/soundbanksinfo.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | AudioKinetic WWise SoundbanksInfo.xml representation and reading. 4 | 5 | See assets/cozmo_resources/sound/SoundbanksInfo.xml 6 | 7 | """ 8 | 9 | from typing import Dict, Any, Union, TextIO 10 | import xml.etree.ElementTree as et # noqa 11 | 12 | from . import exception 13 | 14 | 15 | __all__ = [ 16 | "EventInfo", 17 | "FileInfo", 18 | "SoundBankInfo", 19 | 20 | "load_soundbanksinfo", 21 | ] 22 | 23 | 24 | class EventInfo: 25 | """ Event representation in SoundbanksInfo.xml . """ 26 | 27 | __slots__ = [ 28 | "soundbank_id", 29 | "id", 30 | "name", 31 | "object_path", 32 | ] 33 | 34 | def __init__(self, soundbank_id: int, event_id: int, name: str, object_path: str): 35 | self.soundbank_id = int(soundbank_id) 36 | self.id = int(event_id) 37 | self.name = str(name) 38 | self.object_path = str(object_path) 39 | 40 | 41 | class FileInfo: 42 | """ File representation in SoundbanksInfo.xml . """ 43 | 44 | __slots__ = [ 45 | "soundbank_id", 46 | "id", 47 | "name", 48 | "path", 49 | "embedded", 50 | "prefetch_size", 51 | ] 52 | 53 | def __init__(self, soundbank_id: int, file_id: int, name: str, path: str, embedded: bool, prefetch_size: int): 54 | self.soundbank_id = int(soundbank_id) 55 | self.id = int(file_id) 56 | self.name = str(name) 57 | self.path = str(path) 58 | self.embedded = bool(embedded) 59 | self.prefetch_size = int(prefetch_size) 60 | 61 | def __eq__(self, other: "FileInfo") -> bool: 62 | res = True 63 | res = res and self.soundbank_id == other.soundbank_id 64 | res = res and self.id == other.id 65 | res = res and self.name == other.name 66 | # There are many files that are both embedded and streamed. 67 | # res = res and self.embedded == other.embedded 68 | # res = res and self.prefetch_size == other.prefetch_size 69 | return res 70 | 71 | 72 | class SoundBankInfo: 73 | """ SoundBank representation in SoundbanksInfo.xml . """ 74 | 75 | __slots__ = [ 76 | "id", 77 | "name", 78 | "path", 79 | "language", 80 | "object_path", 81 | ] 82 | 83 | def __init__(self, soundbank_id: int, name: str, path: str, language: str, object_path: str): 84 | self.id = int(soundbank_id) 85 | self.name = str(name) 86 | self.path = str(path) 87 | self.language = str(language) 88 | self.object_path = str(object_path) 89 | 90 | 91 | def load_soundbanksinfo(fspec: Union[str, TextIO]) -> Dict[int, Any]: 92 | """ Load SoundbanksInfo.xml and return a dictionary of parsed Info objects. """ 93 | 94 | try: 95 | tree = et.parse(fspec) 96 | except et.ParseError as e: 97 | raise exception.AudioKineticFormatError("Failed to parse SoundbanksInfo file.") from e 98 | root = tree.getroot() 99 | 100 | # Load StreamedFiles. 101 | streamed_files = {} 102 | for file in root.findall("./StreamedFiles/File"): 103 | file_id = int(file.get("Id")) 104 | assert file_id not in streamed_files 105 | streamed_files[file_id] = { 106 | "id": file_id, 107 | "language": file.get("Language"), 108 | "name": file.find("ShortName").text, 109 | "path": file.find("Path").text, 110 | } 111 | 112 | # Load SoundBanks 113 | objects = {} 114 | for soundbank_node in root.findall("./SoundBanks/SoundBank"): 115 | # Create SoundBankInfo object. 116 | soundbank_id = int(soundbank_node.get("Id")) 117 | language = soundbank_node.get("Language") 118 | soundbank = SoundBankInfo( 119 | soundbank_id, 120 | soundbank_node.find("ShortName").text, 121 | soundbank_node.find("Path").text, 122 | language, 123 | soundbank_node.find("ObjectPath").text) 124 | assert soundbank_id not in objects 125 | objects[soundbank_id] = soundbank 126 | 127 | # Create EventInfo objects. 128 | events = soundbank_node.findall("./IncludedEvents/Event") 129 | for event_node in events: 130 | event_id = int(event_node.get("Id")) 131 | event = EventInfo( 132 | soundbank_id, 133 | event_id, 134 | event_node.get("Name"), 135 | event_node.get("ObjectPath")) 136 | assert event_id not in objects 137 | objects[event_id] = event 138 | 139 | # Create FileInfo objects for streamed files. 140 | files = soundbank_node.findall("./ReferencedStreamedFiles/File") 141 | for file_node in files: 142 | file_id = int(file_node.get("Id")) 143 | streamed_file = streamed_files[file_id] 144 | # The file and SoundBank languages may differ. 145 | # assert streamed_file["language"] == language 146 | file = FileInfo( 147 | soundbank_id, 148 | file_id, 149 | streamed_file["name"], 150 | streamed_file["path"], 151 | False, 152 | -1) 153 | assert file_id not in objects 154 | objects[file_id] = file 155 | 156 | # Create FileInfo objects for embedded files. 157 | files = soundbank_node.findall("./IncludedMemoryFiles/File") 158 | for file_node in files: 159 | file_id = int(file_node.get("Id")) 160 | # The file and SoundBank languages may differ. 161 | # assert file_node.get("Language") == language 162 | prefetch_size_node = file_node.find("PrefetchSize") 163 | prefetch_size = int(prefetch_size_node.text) if prefetch_size_node is not None else -1 164 | file = FileInfo( 165 | soundbank_id, 166 | file_id, 167 | file_node.find("ShortName").text, 168 | file_node.find("Path").text, 169 | True, 170 | prefetch_size) 171 | # assert file_id not in objects 172 | if file_id in objects: 173 | # Many files exist externally and as a "prefetched" embedded file that is truncated. 174 | assert file == objects[file_id] 175 | if not file.embedded: 176 | objects[file_id] = file 177 | else: 178 | objects[file_id] = file 179 | 180 | return objects 181 | -------------------------------------------------------------------------------- /pycozmo/audiokinetic/tests/test_soundbank.py: -------------------------------------------------------------------------------- 1 | 2 | import unittest 3 | import io 4 | 5 | import pycozmo 6 | 7 | 8 | class TestLoadSoundBank(unittest.TestCase): 9 | 10 | def test_invalid(self): 11 | f = io.BytesIO(b"") 12 | reader = pycozmo.audiokinetic.soundbank.SoundBankReader({}) 13 | with self.assertRaises(pycozmo.audiokinetic.exception.AudioKineticIOError): 14 | reader.load_file(f, "test") 15 | 16 | def test_invalid2(self): 17 | f = io.BytesIO(b"ZZZZZZZZ") 18 | reader = pycozmo.audiokinetic.soundbank.SoundBankReader({}) 19 | with self.assertRaises(pycozmo.audiokinetic.exception.AudioKineticFormatError): 20 | reader.load_file(f, "test") 21 | 22 | def test_empty(self): 23 | f = io.BytesIO(b"BKHD\x20\0\0\0\x78\0\0\0\x11\x22\x33\x44\0\0\0\0\0\0\0\0\x6b\x0a\0\0\0\0\0\0\0\0\0\0\0\0\0\0") 24 | reader = pycozmo.audiokinetic.soundbank.SoundBankReader({}) 25 | soundbank = reader.load_file(f, "test") 26 | self.assertEqual(soundbank.fspec, "test") 27 | self.assertEqual(soundbank.id, 0x44332211) 28 | self.assertEqual(soundbank.name, "") 29 | self.assertEqual(soundbank.version, 120) 30 | self.assertEqual(soundbank.data_offset, -1) 31 | self.assertEqual(len(soundbank.objs), 0) 32 | 33 | def test_data_section(self): 34 | f = io.BytesIO( 35 | b"BKHD\x20\0\0\0\x78\0\0\0\x11\x22\x33\x44\0\0\0\0\0\0\0\0\x6b\x0a\0\0\0\0\0\0\0\0\0\0\0\0\0\0" 36 | b"DATA\0\0\0\0") 37 | reader = pycozmo.audiokinetic.soundbank.SoundBankReader({}) 38 | soundbank = reader.load_file(f, "test") 39 | self.assertEqual(soundbank.data_offset, f.tell()) 40 | 41 | def test_data_index_empty(self): 42 | f = io.BytesIO( 43 | b"BKHD\x20\0\0\0\x78\0\0\0\x11\x22\x33\x44\0\0\0\0\0\0\0\0\x6b\x0a\0\0\0\0\0\0\0\0\0\0\0\0\0\0" 44 | b"DIDX\0\0\0\0") 45 | reader = pycozmo.audiokinetic.soundbank.SoundBankReader({}) 46 | soundbank = reader.load_file(f, "test") 47 | self.assertEqual(len(soundbank.objs), 0) 48 | 49 | def test_data_index_invalid(self): 50 | f = io.BytesIO( 51 | b"BKHD\x20\0\0\0\x78\0\0\0\x11\x22\x33\x44\0\0\0\0\0\0\0\0\x6b\x0a\0\0\0\0\0\0\0\0\0\0\0\0\0\0" 52 | b"DIDX\xff\0\0\0") 53 | reader = pycozmo.audiokinetic.soundbank.SoundBankReader({}) 54 | with self.assertRaises(pycozmo.audiokinetic.exception.AudioKineticIOError): 55 | reader.load_file(f, "test") 56 | 57 | def test_data_index(self): 58 | f = io.BytesIO( 59 | b"BKHD\x20\0\0\0\x78\0\0\0\x11\x22\x33\x44\0\0\0\0\0\0\0\0\x6b\x0a\0\0\0\0\0\0\0\0\0\0\0\0\0\0" 60 | b"DIDX\x0c\0\0\0\x01\0\0\0\x02\0\0\0\x03\0\0\0") 61 | reader = pycozmo.audiokinetic.soundbank.SoundBankReader({}) 62 | soundbank = reader.load_file(f, "test") 63 | self.assertEqual(len(soundbank.objs), 1) 64 | file = soundbank.objs[1] 65 | self.assertIsInstance(file, pycozmo.audiokinetic.soundbank.File) 66 | self.assertEqual(file.soundbank_id, soundbank.id) 67 | self.assertEqual(file.id, 1) 68 | self.assertEqual(file.offset, 2) 69 | self.assertEqual(file.length, 3) 70 | 71 | def test_hirc_empty(self): 72 | f = io.BytesIO( 73 | b"BKHD\x20\0\0\0\x78\0\0\0\x11\x22\x33\x44\0\0\0\0\0\0\0\0\x6b\x0a\0\0\0\0\0\0\0\0\0\0\0\0\0\0" 74 | b"HIRC\x04\0\0\0\0\0\0\0") 75 | reader = pycozmo.audiokinetic.soundbank.SoundBankReader({}) 76 | soundbank = reader.load_file(f, "test") 77 | self.assertEqual(len(soundbank.objs), 0) 78 | 79 | def test_hirc_invalid(self): 80 | f = io.BytesIO( 81 | b"BKHD\x20\0\0\0\x78\0\0\0\x11\x22\x33\x44\0\0\0\0\0\0\0\0\x6b\x0a\0\0\0\0\0\0\0\0\0\0\0\0\0\0" 82 | b"HIRC\xff\0\0\0\xff\0\0\0") 83 | reader = pycozmo.audiokinetic.soundbank.SoundBankReader({}) 84 | with self.assertRaises(pycozmo.audiokinetic.exception.AudioKineticIOError): 85 | reader.load_file(f, "test") 86 | 87 | def test_event(self): 88 | f = io.BytesIO( 89 | b"BKHD\x20\0\0\0\x78\0\0\0\x11\x22\x33\x44\0\0\0\0\0\0\0\0\x6b\x0a\0\0\0\0\0\0\0\0\0\0\0\0\0\0" 90 | b"HIRC\x15\0\0\0\x01\0\0\0\x04\x0c\0\0\0\x01\0\0\0\x01\0\0\0\x02\0\0\0") 91 | reader = pycozmo.audiokinetic.soundbank.SoundBankReader({}) 92 | soundbank = reader.load_file(f, "test") 93 | self.assertEqual(len(soundbank.objs), 1) 94 | event = soundbank.objs[1] 95 | self.assertIsInstance(event, pycozmo.audiokinetic.soundbank.Event) 96 | self.assertEqual(event.soundbank_id, soundbank.id) 97 | self.assertEqual(event.id, 1) 98 | self.assertEqual(event.name, "") 99 | self.assertEqual(len(event.action_ids), 1) 100 | self.assertEqual(event.action_ids[0], 2) 101 | 102 | def test_event_action(self): 103 | f = io.BytesIO( 104 | b"BKHD\x20\0\0\0\x78\0\0\0\x11\x22\x33\x44\0\0\0\0\0\0\0\0\x6b\x0a\0\0\0\0\0\0\0\0\0\0\0\0\0\0" 105 | b"HIRC\x14\0\0\0\x01\0\0\0\x03\x0b\0\0\0\x01\0\0\0\x03\x04\xdd\xcc\xbb\xaa\0") 106 | reader = pycozmo.audiokinetic.soundbank.SoundBankReader({}) 107 | soundbank = reader.load_file(f, "test") 108 | self.assertEqual(len(soundbank.objs), 1) 109 | event_action = soundbank.objs[1] 110 | self.assertIsInstance(event_action, pycozmo.audiokinetic.soundbank.EventAction) 111 | self.assertEqual(event_action.soundbank_id, soundbank.id) 112 | self.assertEqual(event_action.id, 1) 113 | self.assertEqual(event_action.scope, 3) 114 | self.assertEqual(event_action.type, 4) 115 | self.assertEqual(event_action.reference_id, 0xaabbccdd) 116 | 117 | def test_sfx(self): 118 | f = io.BytesIO( 119 | b"BKHD\x20\0\0\0\x78\0\0\0\x11\x22\x33\x44\0\0\0\0\0\0\0\0\x6b\x0a\0\0\0\0\0\0\0\0\0\0\0\0\0\0" 120 | b"HIRC\x1e\0\0\0\x01\0\0\0\x02\x15\0\0\0\x01\0\0\0\x01\0\0\0\x02\x03\0\0\0\x04\0\0\0\x01\0\0\0") 121 | reader = pycozmo.audiokinetic.soundbank.SoundBankReader({}) 122 | soundbank = reader.load_file(f, "test") 123 | self.assertEqual(len(soundbank.objs), 1) 124 | event_action = soundbank.objs[1] 125 | self.assertIsInstance(event_action, pycozmo.audiokinetic.soundbank.SFX) 126 | self.assertEqual(event_action.soundbank_id, soundbank.id) 127 | self.assertEqual(event_action.id, 1) 128 | self.assertEqual(event_action.name, "") 129 | self.assertEqual(event_action.location, 2) 130 | self.assertEqual(event_action.file_id, 3) 131 | self.assertEqual(event_action.length, 4) 132 | self.assertEqual(event_action.type, 1) 133 | -------------------------------------------------------------------------------- /pycozmo/event.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | Event declaration and dispatching. 4 | 5 | """ 6 | 7 | from typing import Callable, Optional 8 | import collections 9 | import threading 10 | 11 | from . import exception 12 | from . import robot 13 | 14 | 15 | __all__ = [ 16 | "Event", 17 | 18 | "EvtRobotFound", 19 | "EvtRobotReady", 20 | "EvtPacketReceived", 21 | "EvtNewRawCameraImage", 22 | "EvtRobotMovingChange", 23 | "EvtRobotCarryingBlockChange", 24 | "EvtRobotPickingOrPlacingChange", 25 | "EvtRobotPickedUpChange", 26 | "EvtRobotBodyAccModeChange", 27 | "EvtRobotFallingChange", 28 | "EvtRobotAnimatingChange", 29 | "EvtRobotPathingChange", 30 | "EvtRobotLiftInPositionChange", 31 | "EvtRobotHeadInPositionChange", 32 | "EvtRobotAnimBufferFullChange", 33 | "EvtRobotAnimatingIdleChange", 34 | "EvtRobotOnChargerChange", 35 | "EvtRobotChargingChange", 36 | "EvtCliffDetectedChange", 37 | "EvtRobotWheelsMovingChange", 38 | "EvtChargerOOSChange", 39 | "EvtRobotStateUpdated", 40 | "EvtRobotOrientationChange", 41 | "EvtAudioCompleted", 42 | "EvtAnimationCompleted", 43 | "EvtReactionTrigger", 44 | "EvtBehaviorDone", 45 | 46 | "STATUS_EVENTS", 47 | 48 | "Handler", 49 | "Dispatcher", 50 | ] 51 | 52 | 53 | class Event(object): 54 | """ Base class for events. """ 55 | 56 | 57 | class Handler(object): 58 | """ Event handler class. """ 59 | def __init__(self, f: Callable, one_shot: bool): 60 | self.f = f 61 | self.one_shot = one_shot 62 | 63 | 64 | class EvtRobotFound(Event): 65 | """ Triggered when the robot has been first connected. """ 66 | 67 | 68 | class EvtRobotReady(Event): 69 | """ Triggered when the robot has been initialized and is ready for commands. """ 70 | 71 | 72 | class EvtPacketReceived(Event): 73 | """ Triggered when a new packet has been received from the robot. """ 74 | 75 | 76 | class EvtNewRawCameraImage(Event): 77 | """ Triggered when a new raw image is received from the robot's camera. """ 78 | 79 | 80 | class EvtRobotMovingChange(Event): 81 | pass 82 | 83 | 84 | class EvtRobotCarryingBlockChange(Event): 85 | pass 86 | 87 | 88 | class EvtRobotPickingOrPlacingChange(Event): 89 | pass 90 | 91 | 92 | class EvtRobotPickedUpChange(Event): 93 | pass 94 | 95 | 96 | class EvtRobotBodyAccModeChange(Event): 97 | pass 98 | 99 | 100 | class EvtRobotFallingChange(Event): 101 | pass 102 | 103 | 104 | class EvtRobotAnimatingChange(Event): 105 | pass 106 | 107 | 108 | class EvtRobotPathingChange(Event): 109 | pass 110 | 111 | 112 | class EvtRobotLiftInPositionChange(Event): 113 | pass 114 | 115 | 116 | class EvtRobotHeadInPositionChange(Event): 117 | pass 118 | 119 | 120 | class EvtRobotAnimBufferFullChange(Event): 121 | pass 122 | 123 | 124 | class EvtRobotAnimatingIdleChange(Event): 125 | pass 126 | 127 | 128 | class EvtRobotOnChargerChange(Event): 129 | pass 130 | 131 | 132 | class EvtRobotChargingChange(Event): 133 | pass 134 | 135 | 136 | class EvtCliffDetectedChange(Event): 137 | pass 138 | 139 | 140 | class EvtRobotWheelsMovingChange(Event): 141 | pass 142 | 143 | 144 | class EvtChargerOOSChange(Event): 145 | pass 146 | 147 | 148 | STATUS_EVENTS = { 149 | robot.RobotStatusFlag.IS_MOVING: EvtRobotMovingChange, 150 | robot.RobotStatusFlag.IS_CARRYING_BLOCK: EvtRobotCarryingBlockChange, 151 | robot.RobotStatusFlag.IS_PICKING_OR_PLACING: EvtRobotPickingOrPlacingChange, 152 | robot.RobotStatusFlag.IS_PICKED_UP: EvtRobotPickedUpChange, 153 | robot.RobotStatusFlag.IS_BODY_ACC_MODE: EvtRobotBodyAccModeChange, 154 | robot.RobotStatusFlag.IS_FALLING: EvtRobotFallingChange, 155 | robot.RobotStatusFlag.IS_ANIMATING: EvtRobotAnimatingChange, 156 | robot.RobotStatusFlag.IS_PATHING: EvtRobotPathingChange, 157 | robot.RobotStatusFlag.LIFT_IN_POS: EvtRobotLiftInPositionChange, 158 | robot.RobotStatusFlag.HEAD_IN_POS: EvtRobotHeadInPositionChange, 159 | robot.RobotStatusFlag.IS_ANIM_BUFFER_FULL: EvtRobotAnimBufferFullChange, 160 | robot.RobotStatusFlag.IS_ANIMATING_IDLE: EvtRobotAnimatingChange, 161 | robot.RobotStatusFlag.IS_ON_CHARGER: EvtRobotOnChargerChange, 162 | robot.RobotStatusFlag.IS_CHARGING: EvtRobotChargingChange, 163 | robot.RobotStatusFlag.CLIFF_DETECTED: EvtCliffDetectedChange, 164 | robot.RobotStatusFlag.ARE_WHEELS_MOVING: EvtRobotWheelsMovingChange, 165 | robot.RobotStatusFlag.IS_CHARGER_OOS: EvtChargerOOSChange, 166 | } 167 | 168 | 169 | class EvtRobotStateUpdated(Event): 170 | """ Triggered when a new robot state is received. """ 171 | 172 | 173 | class EvtRobotOrientationChange(Event): 174 | """ Triggered when the robot orientation changes. """ 175 | 176 | 177 | class EvtAudioCompleted(Event): 178 | pass 179 | 180 | 181 | class EvtAnimationCompleted(Event): 182 | pass 183 | 184 | 185 | class EvtReactionTrigger(Event): 186 | pass 187 | 188 | 189 | class EvtBehaviorDone(Event): 190 | pass 191 | 192 | 193 | class Dispatcher(object): 194 | """ Event dispatcher class. """ 195 | 196 | def __init__(self): 197 | super().__init__() 198 | self.dispatch_children = [] 199 | self.dispatch_handlers = collections.defaultdict(list) 200 | 201 | def add_child_dispatcher(self, child): 202 | self.dispatch_children.append(child) 203 | 204 | def del_child_dispatcher(self, child): 205 | try: 206 | self.dispatch_children.remove(child) 207 | except ValueError: 208 | pass 209 | 210 | def add_handler(self, event, f, one_shot=False): 211 | handler = Handler(f, one_shot=one_shot) 212 | self.dispatch_handlers[event].append(handler) 213 | return handler 214 | 215 | def del_handler(self, event, handler): 216 | for i, _handler in enumerate(self.dispatch_handlers[event]): 217 | if _handler == handler: 218 | del self.dispatch_handlers[event][i] 219 | return 220 | 221 | def del_all_handlers(self): 222 | self.dispatch_handlers = collections.defaultdict(list) 223 | 224 | def dispatch(self, event, *args, **kwargs): 225 | # Dispatch to handlers. 226 | handlers = [] 227 | for i, handler in enumerate(self.dispatch_handlers[event]): 228 | if handler.one_shot: 229 | # Delete one-shot handlers prior to actual dispatch 230 | del self.dispatch_handlers[event][i] 231 | handlers.append(handler) 232 | for handler in handlers: 233 | handler.f(*args, **kwargs) 234 | # Dispatch to child dispatchers. 235 | for child in self.dispatch_children: 236 | child.dispatch(event, *args, **kwargs) 237 | 238 | def wait_for(self, evt, timeout: Optional[float] = None) -> None: 239 | e = threading.Event() 240 | self.add_handler(evt, lambda *args: e.set(), one_shot=True) 241 | if not e.wait(timeout): 242 | raise exception.Timeout("Failed to receive event in time.") 243 | -------------------------------------------------------------------------------- /pycozmo/CozmoAnim/BackpackLights.py: -------------------------------------------------------------------------------- 1 | # automatically generated by the FlatBuffers compiler, do not modify 2 | 3 | # namespace: CozmoAnim 4 | 5 | import flatbuffers 6 | 7 | class BackpackLights(object): 8 | __slots__ = ['_tab'] 9 | 10 | @classmethod 11 | def GetRootAsBackpackLights(cls, buf, offset): 12 | n = flatbuffers.encode.Get(flatbuffers.packer.uoffset, buf, offset) 13 | x = BackpackLights() 14 | x.Init(buf, n + offset) 15 | return x 16 | 17 | # BackpackLights 18 | def Init(self, buf, pos): 19 | self._tab = flatbuffers.table.Table(buf, pos) 20 | 21 | # BackpackLights 22 | def TriggerTimeMs(self): 23 | o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(4)) 24 | if o != 0: 25 | return self._tab.Get(flatbuffers.number_types.Uint32Flags, o + self._tab.Pos) 26 | return 0 27 | 28 | # BackpackLights 29 | def DurationTimeMs(self): 30 | o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(6)) 31 | if o != 0: 32 | return self._tab.Get(flatbuffers.number_types.Uint32Flags, o + self._tab.Pos) 33 | return 0 34 | 35 | # BackpackLights 36 | def Left(self, j): 37 | o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(8)) 38 | if o != 0: 39 | a = self._tab.Vector(o) 40 | return self._tab.Get(flatbuffers.number_types.Float32Flags, a + flatbuffers.number_types.UOffsetTFlags.py_type(j * 4)) 41 | return 0 42 | 43 | # BackpackLights 44 | def LeftAsNumpy(self): 45 | o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(8)) 46 | if o != 0: 47 | return self._tab.GetVectorAsNumpy(flatbuffers.number_types.Float32Flags, o) 48 | return 0 49 | 50 | # BackpackLights 51 | def LeftLength(self): 52 | o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(8)) 53 | if o != 0: 54 | return self._tab.VectorLen(o) 55 | return 0 56 | 57 | # BackpackLights 58 | def Right(self, j): 59 | o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(10)) 60 | if o != 0: 61 | a = self._tab.Vector(o) 62 | return self._tab.Get(flatbuffers.number_types.Float32Flags, a + flatbuffers.number_types.UOffsetTFlags.py_type(j * 4)) 63 | return 0 64 | 65 | # BackpackLights 66 | def RightAsNumpy(self): 67 | o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(10)) 68 | if o != 0: 69 | return self._tab.GetVectorAsNumpy(flatbuffers.number_types.Float32Flags, o) 70 | return 0 71 | 72 | # BackpackLights 73 | def RightLength(self): 74 | o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(10)) 75 | if o != 0: 76 | return self._tab.VectorLen(o) 77 | return 0 78 | 79 | # BackpackLights 80 | def Front(self, j): 81 | o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(12)) 82 | if o != 0: 83 | a = self._tab.Vector(o) 84 | return self._tab.Get(flatbuffers.number_types.Float32Flags, a + flatbuffers.number_types.UOffsetTFlags.py_type(j * 4)) 85 | return 0 86 | 87 | # BackpackLights 88 | def FrontAsNumpy(self): 89 | o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(12)) 90 | if o != 0: 91 | return self._tab.GetVectorAsNumpy(flatbuffers.number_types.Float32Flags, o) 92 | return 0 93 | 94 | # BackpackLights 95 | def FrontLength(self): 96 | o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(12)) 97 | if o != 0: 98 | return self._tab.VectorLen(o) 99 | return 0 100 | 101 | # BackpackLights 102 | def Middle(self, j): 103 | o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(14)) 104 | if o != 0: 105 | a = self._tab.Vector(o) 106 | return self._tab.Get(flatbuffers.number_types.Float32Flags, a + flatbuffers.number_types.UOffsetTFlags.py_type(j * 4)) 107 | return 0 108 | 109 | # BackpackLights 110 | def MiddleAsNumpy(self): 111 | o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(14)) 112 | if o != 0: 113 | return self._tab.GetVectorAsNumpy(flatbuffers.number_types.Float32Flags, o) 114 | return 0 115 | 116 | # BackpackLights 117 | def MiddleLength(self): 118 | o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(14)) 119 | if o != 0: 120 | return self._tab.VectorLen(o) 121 | return 0 122 | 123 | # BackpackLights 124 | def Back(self, j): 125 | o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(16)) 126 | if o != 0: 127 | a = self._tab.Vector(o) 128 | return self._tab.Get(flatbuffers.number_types.Float32Flags, a + flatbuffers.number_types.UOffsetTFlags.py_type(j * 4)) 129 | return 0 130 | 131 | # BackpackLights 132 | def BackAsNumpy(self): 133 | o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(16)) 134 | if o != 0: 135 | return self._tab.GetVectorAsNumpy(flatbuffers.number_types.Float32Flags, o) 136 | return 0 137 | 138 | # BackpackLights 139 | def BackLength(self): 140 | o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(16)) 141 | if o != 0: 142 | return self._tab.VectorLen(o) 143 | return 0 144 | 145 | def BackpackLightsStart(builder): builder.StartObject(7) 146 | def BackpackLightsAddTriggerTimeMs(builder, triggerTimeMs): builder.PrependUint32Slot(0, triggerTimeMs, 0) 147 | def BackpackLightsAddDurationTimeMs(builder, durationTimeMs): builder.PrependUint32Slot(1, durationTimeMs, 0) 148 | def BackpackLightsAddLeft(builder, Left): builder.PrependUOffsetTRelativeSlot(2, flatbuffers.number_types.UOffsetTFlags.py_type(Left), 0) 149 | def BackpackLightsStartLeftVector(builder, numElems): return builder.StartVector(4, numElems, 4) 150 | def BackpackLightsAddRight(builder, Right): builder.PrependUOffsetTRelativeSlot(3, flatbuffers.number_types.UOffsetTFlags.py_type(Right), 0) 151 | def BackpackLightsStartRightVector(builder, numElems): return builder.StartVector(4, numElems, 4) 152 | def BackpackLightsAddFront(builder, Front): builder.PrependUOffsetTRelativeSlot(4, flatbuffers.number_types.UOffsetTFlags.py_type(Front), 0) 153 | def BackpackLightsStartFrontVector(builder, numElems): return builder.StartVector(4, numElems, 4) 154 | def BackpackLightsAddMiddle(builder, Middle): builder.PrependUOffsetTRelativeSlot(5, flatbuffers.number_types.UOffsetTFlags.py_type(Middle), 0) 155 | def BackpackLightsStartMiddleVector(builder, numElems): return builder.StartVector(4, numElems, 4) 156 | def BackpackLightsAddBack(builder, Back): builder.PrependUOffsetTRelativeSlot(6, flatbuffers.number_types.UOffsetTFlags.py_type(Back), 0) 157 | def BackpackLightsStartBackVector(builder, numElems): return builder.StartVector(4, numElems, 4) 158 | def BackpackLightsEnd(builder): return builder.EndObject() 159 | -------------------------------------------------------------------------------- /pycozmo/window.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | Cozmo protocol sliding window implementation. 4 | 5 | """ 6 | 7 | import math 8 | from typing import Optional, List, Tuple, Any 9 | 10 | from . import exception 11 | 12 | 13 | __all__ = [ 14 | "BaseWindow", 15 | "ReceiveWindow", 16 | "SendWindow", 17 | ] 18 | 19 | 20 | class BaseWindow(object): 21 | """ Base communication window class. """ 22 | 23 | def __init__(self, seq_bits: int, size: Optional[int] = None, max_seq: Optional[int] = None) -> None: 24 | """ Crate a window by specifying either sequence number bits or size of the window. """ 25 | if size is None: 26 | if seq_bits < 1: 27 | raise ValueError("Invalid sequence number bits.") 28 | size = int(math.pow(2, seq_bits - 1)) 29 | elif size <= 0 or size > int(math.pow(2, seq_bits - 1)): 30 | raise ValueError("Invalid window size.") 31 | # Size of the window. 32 | self.size = size 33 | # Next expected sequence number (0, max_seq-1). 34 | self.expected_seq = 0 35 | # Maximum sequence number (first invalid). 36 | self.max_seq = max_seq or int(math.pow(2, seq_bits)) 37 | if self.max_seq % self.size != 0: 38 | raise ValueError("The maximum sequence number must be a multiple of the window size must.") 39 | # Window data 40 | self.window = [None for _ in range(self.size)] 41 | 42 | def is_valid_seq(self, seq: int) -> bool: 43 | """ Check whether a sequence number is valid for the window. """ 44 | res = 0 <= seq < self.max_seq 45 | return res 46 | 47 | def reset(self) -> None: 48 | """ Reset the window. """ 49 | self.expected_seq = 0 50 | self.window = [None for _ in range(self.size)] 51 | 52 | 53 | class ReceiveWindow(BaseWindow): 54 | """ 55 | Receive communication window class. 56 | 57 | When packets are received (in whatever order), they are put in the window using the put() method. 58 | 59 | Packets are extracted from the window in the expected order using the get() method. 60 | """ 61 | 62 | def __init__(self, seq_bits: int, size: Optional[int] = None, max_seq: Optional[int] = None) -> None: 63 | """ Crate a window by specifying either sequence number bits or size of the window. """ 64 | super().__init__(seq_bits, size, max_seq) 65 | # Last used sequence number (0, max_seq-1). 66 | self.last_seq = (self.expected_seq + self.size - 1) % self.max_seq 67 | 68 | def is_out_of_order(self, seq: int) -> bool: 69 | """ Check whether a sequence number is outside the current window (assuming it is valid). """ 70 | if self.expected_seq > self.last_seq: 71 | res = self.expected_seq > seq > self.last_seq 72 | else: 73 | res = seq < self.expected_seq or seq > self.last_seq 74 | return res 75 | 76 | def exists(self, seq: int) -> bool: 77 | """ Check whether a sequence number has already been received (assuming it is valid). """ 78 | res = self.window[seq % self.size] is not None 79 | return res 80 | 81 | def put(self, seq: int, data: Any) -> None: 82 | """ Add the data, associated with a particular sequence number to the window. """ 83 | if not self.is_valid_seq(seq): 84 | # Invalid sequence number. 85 | return 86 | if self.is_out_of_order(seq): 87 | # Not in the window. 88 | return 89 | if self.exists(seq): 90 | # Duplicate. 91 | return 92 | self.window[seq % self.size] = data 93 | 94 | def get(self) -> Any: 95 | """ If data is available, return it and move the window forward. Return None otherwise. """ 96 | data = self.window[self.expected_seq % self.size] 97 | if data is not None: 98 | self.window[self.expected_seq % self.size] = None 99 | self.expected_seq = (self.expected_seq + 1) % self.max_seq 100 | self.last_seq = (self.expected_seq + self.size - 1) % self.max_seq 101 | return data 102 | 103 | def reset(self) -> None: 104 | """ Reset the window. """ 105 | super().reset() 106 | self.last_seq = (self.expected_seq + self.size - 1) % self.max_seq 107 | 108 | 109 | class SendWindow(BaseWindow): 110 | """ 111 | Send communication window class. 112 | 113 | When packets are sent, they are put in the window using the put() method which returns a sequence number. 114 | 115 | Packets are removed from the window when they are acknowledged with the acknowledge() method. 116 | """ 117 | 118 | def __init__(self, seq_bits: int, size: Optional[int] = None, max_seq: Optional[int] = None) -> None: 119 | """ Crate a window by specifying either sequence number bits or size of the window. """ 120 | super().__init__(seq_bits, size, max_seq) 121 | self.next_seq = 0 122 | 123 | def is_out_of_order(self, seq: int) -> bool: 124 | """ Check whether a sequence number is outside the current window (assuming it is valid). """ 125 | if self.expected_seq > self.next_seq: 126 | res = self.expected_seq > seq >= self.next_seq 127 | else: 128 | res = seq < self.expected_seq or seq >= self.next_seq 129 | return res 130 | 131 | def is_full(self) -> bool: 132 | """ Check whether the window is full. """ 133 | if self.expected_seq > self.next_seq: 134 | res = (self.next_seq + self.max_seq - self.expected_seq) >= self.size 135 | else: 136 | res = (self.next_seq - self.expected_seq) >= self.size 137 | return res 138 | 139 | def put(self, data: Any) -> int: 140 | """ Add data to the window. Raises NoSpace exception if the window is full. """ 141 | if self.is_full(): 142 | raise exception.NoSpace("Send window full.") 143 | self.window[self.next_seq % self.size] = data 144 | seq = self.next_seq 145 | self.next_seq = (self.next_seq + 1) % self.max_seq 146 | return seq 147 | 148 | def acknowledge(self, seq: int) -> None: 149 | """ Acknowledge a sequence number and remove any associated data from the window. """ 150 | if not self.is_valid_seq(seq): 151 | # Invalid sequence number. 152 | return 153 | if self.is_out_of_order(seq): 154 | # Not in the window. 155 | return 156 | seq = (seq + 1) % self.max_seq 157 | while self.expected_seq != seq: 158 | self.window[self.expected_seq % self.size] = None 159 | self.expected_seq = (self.expected_seq + 1) % self.max_seq 160 | 161 | def get(self) -> List[Tuple[int, Any]]: 162 | """ Get the contents of the window as a list of tuples (sequence number, data). """ 163 | res = [] 164 | seq = self.expected_seq 165 | while seq != self.next_seq: 166 | res.append((seq, self.window[seq % self.size])) 167 | seq = (seq + 1) % self.max_seq 168 | return res 169 | 170 | def reset(self): 171 | """ Reset the window. """ 172 | super().reset() 173 | self.next_seq = 0 174 | -------------------------------------------------------------------------------- /pycozmo/frame.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | Cozmo protocol frame representation and encoding and decoding. 4 | 5 | """ 6 | 7 | from typing import List 8 | 9 | from .logger import logger_protocol 10 | from .protocol_ast import FrameType, PacketType 11 | from .protocol_declaration import FRAME_ID, MIN_FRAME_SIZE, MAX_SEQ, OOB_SEQ 12 | from .protocol_base import Packet, UnknownCommand, UnknownEvent 13 | from .protocol_utils import BinaryReader, BinaryWriter 14 | from .protocol_encoder import Connect, Disconnect, Ping, Keyframe, PACKETS_BY_ID 15 | 16 | 17 | __all__ = [ 18 | "Frame", 19 | ] 20 | 21 | 22 | class Frame(object): 23 | """ Cozmo protocol frame. """ 24 | 25 | __slots__ = [ 26 | 'type', 27 | 'first_seq', 28 | 'seq', 29 | 'ack', 30 | 'pkts', 31 | ] 32 | 33 | def __init__(self, type_id: FrameType, first_seq: int, seq: int, ack: int, pkts: List[Packet]) -> None: 34 | self.type = type_id 35 | self.first_seq = first_seq 36 | self.seq = seq 37 | self.ack = ack 38 | self.pkts = pkts 39 | 40 | def to_bytes(self) -> bytes: 41 | writer = BinaryWriter() 42 | self.to_writer(writer) 43 | return writer.dumps() 44 | 45 | @staticmethod 46 | def _encode_packet(pkt: Packet, writer: BinaryWriter) -> None: 47 | writer.write(pkt.type.value, "B") 48 | if pkt.type == PacketType.COMMAND or pkt.type == PacketType.EVENT: 49 | writer.write(len(pkt) + 1, "H") 50 | writer.write(pkt.id, "B") 51 | else: 52 | writer.write(len(pkt), "H") 53 | writer.write_object(pkt) 54 | 55 | def to_writer(self, writer: BinaryWriter) -> None: 56 | writer.write_bytes(FRAME_ID) 57 | writer.write(self.type.value, "B") 58 | writer.write((self.first_seq + 1) % 0x10000, "H") 59 | writer.write((self.seq + 1) % 0x10000, "H") 60 | writer.write((self.ack + 1) % 0x10000, "H") 61 | if self.type == FrameType.ENGINE or self.type == FrameType.ROBOT: 62 | for pkt in self.pkts: 63 | self._encode_packet(pkt, writer) 64 | elif self.type == FrameType.PING: 65 | assert len(self.pkts) == 1 66 | pkt = self.pkts[0] 67 | assert pkt.type == PacketType.PING 68 | writer.write_object(pkt) 69 | elif self.type == FrameType.ENGINE_ACT: 70 | assert len(self.pkts) == 1 71 | pkt = self.pkts[0] 72 | assert pkt.type in (PacketType.COMMAND, PacketType.DISCONNECT) 73 | writer.write(pkt.id, "B") 74 | writer.write_object(pkt) 75 | elif self.type == FrameType.RESET: 76 | # No packets 77 | assert not self.pkts 78 | elif self.type == FrameType.FIN: 79 | # No packets 80 | assert not self.pkts 81 | else: 82 | raise NotImplementedError("Unexpected frame type {}.".format(self.type)) 83 | 84 | @classmethod 85 | def from_bytes(cls, buffer: bytes) -> "Frame": 86 | reader = BinaryReader(buffer) 87 | obj = cls.from_reader(reader) 88 | return obj 89 | 90 | @classmethod 91 | def _decode_packet(cls, pkt_type, pkt_len, reader): 92 | if pkt_type == PacketType.COMMAND or pkt_type == PacketType.EVENT: 93 | pkt_id = reader.read("B") 94 | pkt_class = PACKETS_BY_ID.get(pkt_id) # type: Packet # type: ignore 95 | if pkt_class: 96 | res = pkt_class.from_reader(reader) 97 | elif pkt_type == PacketType.COMMAND: 98 | res = UnknownCommand(pkt_id, reader.read_farray("B", pkt_len - 1)) 99 | else: 100 | res = UnknownEvent(pkt_id, reader.read_farray("B", pkt_len - 1)) 101 | elif pkt_type == PacketType.PING: 102 | res = Ping.from_reader(reader) 103 | elif pkt_type == PacketType.KEYFRAME: 104 | res = Keyframe.from_reader(reader) 105 | elif pkt_type == PacketType.CONNECT: 106 | res = Connect.from_reader(reader) 107 | elif pkt_type == PacketType.DISCONNECT: 108 | res = Disconnect.from_reader(reader) 109 | else: 110 | raise ValueError("Unexpected packet type {}.".format(pkt_type)) 111 | return res 112 | 113 | @classmethod 114 | def from_reader(cls, reader: BinaryReader) -> "Frame": 115 | if len(reader.buffer) < MIN_FRAME_SIZE: 116 | raise ValueError("Invalid frame.") 117 | 118 | if reader.buffer[:7] != FRAME_ID: 119 | raise ValueError("Invalid frame ID.") 120 | 121 | reader.seek_set(7) 122 | frame_type = FrameType(reader.read("B")) 123 | first_seq = (reader.read("H") - 1) % 0x10000 124 | seq = (reader.read("H") - 1) % 0x10000 125 | ack = (reader.read("H") - 1) % 0x10000 126 | pkts = [] 127 | 128 | if frame_type == FrameType.ENGINE or frame_type == FrameType.ROBOT: 129 | pkt_seq = first_seq 130 | while reader.tell() < len(reader): 131 | pkt_type = PacketType(reader.read("B")) 132 | pkt_len = reader.read("H") 133 | expected_offset = reader.tell() + pkt_len 134 | try: 135 | pkt = cls._decode_packet(pkt_type, pkt_len, reader) 136 | if reader.tell() != expected_offset: 137 | # Packet length may change between protocol versions. 138 | reader.seek_set(expected_offset) 139 | pkt.seq = pkt_seq 140 | pkt.ack = ack 141 | if not pkt.is_oob(): 142 | pkt_seq = (pkt_seq + 1) % MAX_SEQ 143 | pkts.append(pkt) 144 | except (ValueError, IndexError) as e: 145 | logger_protocol.debug("Failed to decode packet. Ignoring. {}".format(e)) 146 | reader.seek_set(expected_offset) 147 | assert seq == OOB_SEQ or seq + 1 == pkt_seq or pkt.type == PacketType.PING 148 | elif frame_type == FrameType.PING: 149 | pkt = Ping.from_reader(reader) 150 | pkts.append(pkt) 151 | elif frame_type == FrameType.ENGINE_ACT: 152 | pkt_seq = first_seq 153 | pkt_type = PacketType.COMMAND 154 | pkt_len = len(reader) - reader.tell() 155 | try: 156 | pkt = cls._decode_packet(pkt_type, pkt_len, reader) 157 | pkt.seq = pkt_seq 158 | pkt.ack = ack 159 | if not pkt.is_oob(): 160 | pkt_seq = (pkt_seq + 1) % MAX_SEQ 161 | pkts.append(pkt) 162 | except (ValueError, IndexError) as e: 163 | logger_protocol.debug("Failed to decode packet. Ignoring. {}".format(e)) 164 | assert seq == OOB_SEQ or seq + 1 == pkt_seq 165 | elif frame_type == FrameType.RESET: 166 | # No packets 167 | assert reader.tell() == len(reader) 168 | elif frame_type == FrameType.FIN: 169 | # No packets 170 | assert reader.tell() == len(reader) 171 | else: 172 | raise NotImplementedError("Unexpected frame type {}.".format(frame_type)) 173 | 174 | res = cls(frame_type, first_seq, seq, ack, pkts) 175 | 176 | return res 177 | -------------------------------------------------------------------------------- /pycozmo/camera.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | Camera image decoding. 4 | 5 | """ 6 | 7 | import numpy as np 8 | 9 | from . import protocol_encoder 10 | 11 | 12 | __all__ = [ 13 | "RESOLUTIONS", 14 | 15 | "minigray_to_jpeg", 16 | "minicolor_to_jpeg", 17 | ] 18 | 19 | 20 | #: Camera resolutions. 21 | RESOLUTIONS = { 22 | protocol_encoder.ImageResolution.VerificationSnapshot: (16, 16), 23 | protocol_encoder.ImageResolution.QQQQVGA: (40, 30), 24 | protocol_encoder.ImageResolution.QQQVGA: (80, 60), 25 | protocol_encoder.ImageResolution.QQVGA: (160, 120), 26 | protocol_encoder.ImageResolution.QVGA: (320, 240), 27 | protocol_encoder.ImageResolution.CVGA: (400, 296), 28 | protocol_encoder.ImageResolution.VGA: (640, 480), 29 | protocol_encoder.ImageResolution.SVGA: (800, 600), 30 | protocol_encoder.ImageResolution.XGA: (1024, 768), 31 | protocol_encoder.ImageResolution.SXGA: (1280, 960), 32 | protocol_encoder.ImageResolution.UXGA: (1600, 1200), 33 | protocol_encoder.ImageResolution.QXGA: (2048, 1536), 34 | protocol_encoder.ImageResolution.QUXGA: (3200, 2400) 35 | } 36 | 37 | 38 | def minigray_to_jpeg(minigray, width, height): 39 | """ Converts miniGrayToJpeg format to normal JPEG format. """ 40 | header50 = np.array([ 41 | 0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46, 0x49, 0x46, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 42 | 0x00, 0x01, 0x00, 0x00, 0xFF, 0xDB, 0x00, 0x43, 0x00, 0x10, 0x0B, 0x0C, 0x0E, 0x0C, 0x0A, 0x10, 43 | # // 0x19 = QTable 44 | 0x0E, 0x0D, 0x0E, 0x12, 0x11, 0x10, 0x13, 0x18, 0x28, 0x1A, 0x18, 0x16, 0x16, 0x18, 0x31, 0x23, 45 | 0x25, 0x1D, 0x28, 0x3A, 0x33, 0x3D, 0x3C, 0x39, 0x33, 0x38, 0x37, 0x40, 0x48, 0x5C, 0x4E, 0x40, 46 | 0x44, 0x57, 0x45, 0x37, 0x38, 0x50, 0x6D, 0x51, 0x57, 0x5F, 0x62, 0x67, 0x68, 0x67, 0x3E, 0x4D, 47 | 48 | # //0x71, 0x79, 0x70, 0x64, 0x78, 0x5C, 0x65, 0x67, 0x63, 0xFF, 0xC0, 0x00, 0x0B, 0x08, 0x00, 0xF0, 49 | 0x71, 0x79, 0x70, 0x64, 0x78, 0x5C, 0x65, 0x67, 0x63, 0xFF, 0xC0, 0x00, 0x0B, 0x08, 0x01, 0x28, 50 | # // 0x5E = Height x Width 51 | 52 | # //0x01, 0x40, 0x01, 0x01, 0x11, 0x00, 0xFF, 0xC4, 0x00, 0xD2, 0x00, 0x00, 0x01, 0x05, 0x01, 0x01, 53 | 0x01, 0x90, 0x01, 0x01, 0x11, 0x00, 0xFF, 0xC4, 0x00, 0xD2, 0x00, 0x00, 0x01, 0x05, 0x01, 0x01, 54 | 55 | 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 56 | 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x10, 0x00, 0x02, 0x01, 0x03, 0x03, 0x02, 0x04, 0x03, 57 | 0x05, 0x05, 0x04, 0x04, 0x00, 0x00, 0x01, 0x7D, 0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12, 58 | 0x21, 0x31, 0x41, 0x06, 0x13, 0x51, 0x61, 0x07, 0x22, 0x71, 0x14, 0x32, 0x81, 0x91, 0xA1, 0x08, 59 | 0x23, 0x42, 0xB1, 0xC1, 0x15, 0x52, 0xD1, 0xF0, 0x24, 0x33, 0x62, 0x72, 0x82, 0x09, 0x0A, 0x16, 60 | 0x17, 0x18, 0x19, 0x1A, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 61 | 0x3A, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4A, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 62 | 0x5A, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6A, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 63 | 0x7A, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8A, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 64 | 0x99, 0x9A, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6, 65 | 0xB7, 0xB8, 0xB9, 0xBA, 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7, 0xC8, 0xC9, 0xCA, 0xD2, 0xD3, 0xD4, 66 | 0xD5, 0xD6, 0xD7, 0xD8, 0xD9, 0xDA, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8, 0xE9, 0xEA, 67 | 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8, 0xF9, 0xFA, 0xFF, 0xDA, 0x00, 0x08, 0x01, 0x01, 68 | 0x00, 0x00, 0x3F, 0x00 69 | ], dtype=np.uint8) 70 | 71 | return mini_to_jpeg_helper(minigray, width, height, header50) 72 | 73 | 74 | def minicolor_to_jpeg(minicolor, width, height): 75 | """ Converts miniColorToJpeg format to normal JPEG format. """ 76 | header = np.array([ 77 | 0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46, 0x49, 0x46, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 78 | 0x00, 0x01, 0x00, 0x00, 0xFF, 0xDB, 0x00, 0x43, 0x00, 0x10, 0x0B, 0x0C, 0x0E, 0x0C, 0x0A, 0x10, 79 | # 0x19 = QTable 80 | 0x0E, 0x0D, 0x0E, 0x12, 0x11, 0x10, 0x13, 0x18, 0x28, 0x1A, 0x18, 0x16, 0x16, 0x18, 0x31, 0x23, 81 | 0x25, 0x1D, 0x28, 0x3A, 0x33, 0x3D, 0x3C, 0x39, 0x33, 0x38, 0x37, 0x40, 0x48, 0x5C, 0x4E, 0x40, 82 | 0x44, 0x57, 0x45, 0x37, 0x38, 0x50, 0x6D, 0x51, 0x57, 0x5F, 0x62, 0x67, 0x68, 0x67, 0x3E, 0x4D, 83 | 0x71, 0x79, 0x70, 0x64, 0x78, 0x5C, 0x65, 0x67, 0x63, 0xFF, 0xC0, 0x00, 17, # 8+3*components 84 | 0x08, 0x00, 0xF0, # 0x5E = Height x Width 85 | 0x01, 0x40, 86 | 0x03, # 3 components 87 | 0x01, 0x21, 0x00, # Y 2x1 res 88 | 0x02, 0x11, 0x00, # Cb 89 | 0x03, 0x11, 0x00, # Cr 90 | 0xFF, 0xC4, 0x00, 0xD2, 0x00, 0x00, 0x01, 0x05, 0x01, 0x01, 91 | 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 92 | 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x10, 0x00, 0x02, 0x01, 0x03, 0x03, 0x02, 0x04, 0x03, 93 | 0x05, 0x05, 0x04, 0x04, 0x00, 0x00, 0x01, 0x7D, 0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12, 94 | 0x21, 0x31, 0x41, 0x06, 0x13, 0x51, 0x61, 0x07, 0x22, 0x71, 0x14, 0x32, 0x81, 0x91, 0xA1, 0x08, 95 | 0x23, 0x42, 0xB1, 0xC1, 0x15, 0x52, 0xD1, 0xF0, 0x24, 0x33, 0x62, 0x72, 0x82, 0x09, 0x0A, 0x16, 96 | 0x17, 0x18, 0x19, 0x1A, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 97 | 0x3A, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4A, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 98 | 0x5A, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6A, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 99 | 0x7A, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8A, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 100 | 0x99, 0x9A, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6, 101 | 0xB7, 0xB8, 0xB9, 0xBA, 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7, 0xC8, 0xC9, 0xCA, 0xD2, 0xD3, 0xD4, 102 | 0xD5, 0xD6, 0xD7, 0xD8, 0xD9, 0xDA, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8, 0xE9, 0xEA, 103 | 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8, 0xF9, 0xFA, 104 | 0xFF, 0xDA, 0x00, 12, 105 | 0x03, # 3 components 106 | 0x01, 0x00, # Y 107 | 0x02, 0x00, # Cb same AC/DC 108 | 0x03, 0x00, # Cr same AC/DC 109 | 0x00, 0x3F, 0x00 110 | ], dtype=np.uint8) 111 | 112 | return mini_to_jpeg_helper(minicolor, width, height, header) 113 | 114 | 115 | def mini_to_jpeg_helper(mini, width, height, header): 116 | """ Low-level mini*ToJpeg format to normal JPEG format conversion. """ 117 | buffer_in = mini.tolist() 118 | curr_len = len(mini) 119 | 120 | header_length = len(header) 121 | # For worst case expansion 122 | buffer_out = np.array([0] * (curr_len * 2 + header_length), dtype=np.uint8) 123 | 124 | for i in range(header_length): 125 | buffer_out[i] = header[i] 126 | 127 | buffer_out[0x5e] = height >> 8 128 | buffer_out[0x5f] = height & 0xff 129 | buffer_out[0x60] = width >> 8 130 | buffer_out[0x61] = width & 0xff 131 | # Remove padding at the end 132 | while buffer_in[curr_len - 1] == 0xff: 133 | curr_len -= 1 134 | 135 | off = header_length 136 | for i in range(curr_len - 1): 137 | buffer_out[off] = buffer_in[i + 1] 138 | off += 1 139 | if buffer_in[i + 1] == 0xff: 140 | buffer_out[off] = 0 141 | off += 1 142 | 143 | buffer_out[off] = 0xff 144 | off += 1 145 | buffer_out[off] = 0xD9 146 | 147 | return np.asarray(buffer_out) 148 | --------------------------------------------------------------------------------