├── 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 |
--------------------------------------------------------------------------------