lifx-async 5.0.0__tar.gz → 5.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {lifx_async-5.0.0 → lifx_async-5.1.0}/.github/workflows/ci.yml +47 -5
- {lifx_async-5.0.0 → lifx_async-5.1.0}/.github/workflows/docs.yml +3 -3
- {lifx_async-5.0.0 → lifx_async-5.1.0}/CLAUDE.md +85 -1
- {lifx_async-5.0.0 → lifx_async-5.1.0}/PKG-INFO +1 -1
- lifx_async-5.1.0/docs/api/animation.md +356 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/docs/api/index.md +22 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/docs/api/protocol.md +0 -14
- {lifx_async-5.0.0 → lifx_async-5.1.0}/docs/changelog.md +29 -0
- lifx_async-5.1.0/docs/user-guide/animation.md +369 -0
- lifx_async-5.1.0/examples/05_device_groups.py +205 -0
- lifx_async-5.1.0/examples/15_animation.py +372 -0
- lifx_async-5.1.0/examples/16_animation_numpy.py +543 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/mkdocs.yml +4 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/pyproject.toml +1 -1
- {lifx_async-5.0.0 → lifx_async-5.1.0}/renovate.json +1 -0
- lifx_async-5.1.0/scripts/test_multiversion.py +227 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/src/lifx/__init__.py +4 -0
- lifx_async-5.1.0/src/lifx/animation/__init__.py +87 -0
- lifx_async-5.1.0/src/lifx/animation/animator.py +323 -0
- lifx_async-5.1.0/src/lifx/animation/framebuffer.py +395 -0
- lifx_async-5.1.0/src/lifx/animation/orientation.py +159 -0
- lifx_async-5.1.0/src/lifx/animation/packets.py +497 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/src/lifx/api.py +0 -8
- {lifx_async-5.0.0 → lifx_async-5.1.0}/src/lifx/const.py +18 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/src/lifx/devices/ceiling.py +7 -8
- {lifx_async-5.0.0 → lifx_async-5.1.0}/src/lifx/devices/matrix.py +2 -19
- {lifx_async-5.0.0 → lifx_async-5.1.0}/src/lifx/effects/colorloop.py +2 -2
- {lifx_async-5.0.0 → lifx_async-5.1.0}/src/lifx/network/connection.py +8 -7
- {lifx_async-5.0.0 → lifx_async-5.1.0}/src/lifx/network/discovery.py +3 -3
- {lifx_async-5.0.0 → lifx_async-5.1.0}/src/lifx/network/mdns/transport.py +2 -2
- {lifx_async-5.0.0 → lifx_async-5.1.0}/src/lifx/network/transport.py +8 -3
- lifx_async-5.1.0/src/lifx/network/utils.py +15 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/src/lifx/protocol/serializer.py +0 -85
- lifx_async-5.1.0/tests/test_animation/__init__.py +1 -0
- lifx_async-5.1.0/tests/test_animation/conftest.py +178 -0
- lifx_async-5.1.0/tests/test_animation/test_animator.py +482 -0
- lifx_async-5.1.0/tests/test_animation/test_framebuffer.py +380 -0
- lifx_async-5.1.0/tests/test_animation/test_orientation.py +167 -0
- lifx_async-5.1.0/tests/test_animation/test_packets.py +504 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_api/test_api_apply_theme.py +2 -1
- {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_api/test_api_batch_operations.py +4 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_devices/test_base.py +14 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_devices/test_state_multizone.py +8 -4
- {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_network/test_concurrent_requests.py +36 -16
- {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_protocol/test_serializer.py +0 -161
- {lifx_async-5.0.0 → lifx_async-5.1.0}/uv.lock +4 -4
- {lifx_async-5.0.0 → lifx_async-5.1.0}/.claude/settings.json +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/.github/dependabot.yml +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/.github/labeler.yml +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/.github/workflows/pr-automation.yml +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/.gitignore +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/.pre-commit-config.yaml +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/LICENSE +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/README.md +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/context7.json +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/docs/api/colors.md +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/docs/api/devices.md +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/docs/api/effects.md +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/docs/api/exceptions.md +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/docs/api/high-level.md +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/docs/api/network.md +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/docs/api/themes.md +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/docs/architecture/effects-architecture.md +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/docs/architecture/overview.md +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/docs/faq.md +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/docs/getting-started/effects.md +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/docs/getting-started/installation.md +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/docs/getting-started/quickstart.md +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/docs/getting-started/themes.md +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/docs/index.md +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/docs/migration/effect-api-changes.md +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/docs/stylesheets/extra.css +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/docs/user-guide/advanced-usage.md +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/docs/user-guide/ceiling-lights.md +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/docs/user-guide/effects-custom.md +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/docs/user-guide/effects-troubleshooting.md +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/docs/user-guide/protocol-deep-dive.md +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/docs/user-guide/themes.md +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/docs/user-guide/troubleshooting.md +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/examples/01_simple_discovery.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/examples/02_simple_control.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/examples/03_waveforms.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/examples/04_logging.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/examples/06_pulse_effect.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/examples/07_colorloop_effect.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/examples/08_custom_effect.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/examples/09_background_effect.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/examples/10_find_specific_devices.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/examples/11_matrix_basic.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/examples/12_matrix_effects.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/examples/13_matrix_large.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/examples/14_mdns_discovery.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/scripts/mdns_probe.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/src/lifx/color.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/src/lifx/devices/__init__.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/src/lifx/devices/base.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/src/lifx/devices/hev.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/src/lifx/devices/infrared.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/src/lifx/devices/light.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/src/lifx/devices/multizone.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/src/lifx/effects/__init__.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/src/lifx/effects/base.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/src/lifx/effects/conductor.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/src/lifx/effects/const.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/src/lifx/effects/models.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/src/lifx/effects/pulse.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/src/lifx/effects/state_manager.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/src/lifx/exceptions.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/src/lifx/network/__init__.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/src/lifx/network/mdns/__init__.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/src/lifx/network/mdns/discovery.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/src/lifx/network/mdns/dns.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/src/lifx/network/mdns/types.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/src/lifx/network/message.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/src/lifx/products/__init__.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/src/lifx/products/generator.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/src/lifx/products/quirks.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/src/lifx/products/registry.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/src/lifx/protocol/__init__.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/src/lifx/protocol/base.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/src/lifx/protocol/generator.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/src/lifx/protocol/header.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/src/lifx/protocol/models.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/src/lifx/protocol/packets.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/src/lifx/protocol/protocol_types.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/src/lifx/py.typed +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/src/lifx/theme/__init__.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/src/lifx/theme/canvas.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/src/lifx/theme/generators.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/src/lifx/theme/library.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/src/lifx/theme/theme.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/__init__.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/conftest.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_api/__init__.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_api/test_api_batch_errors.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_api/test_api_discovery.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_api/test_api_organization.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_color.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_devices/__init__.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_devices/conftest.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_devices/test_ceiling.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_devices/test_hev.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_devices/test_infrared.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_devices/test_light.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_devices/test_mac_address.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_devices/test_matrix.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_devices/test_multizone.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_devices/test_state_ceiling.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_devices/test_state_hev.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_devices/test_state_infrared.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_devices/test_state_light.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_devices/test_state_management.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_devices/test_state_matrix.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_effects/__init__.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_effects/test_base.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_effects/test_capability_filtering.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_effects/test_colorloop.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_effects/test_integration.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_effects/test_models.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_effects/test_pulse.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_effects/test_state_manager.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_network/__init__.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_network/test_connection.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_network/test_discovery_devices.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_network/test_discovery_errors.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_network/test_mdns/__init__.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_network/test_mdns/conftest.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_network/test_mdns/test_discovery.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_network/test_mdns/test_dns.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_network/test_mdns/test_transport.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_network/test_message.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_network/test_message_advanced.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_network/test_transport.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_products/test_product_generator.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_products/test_registry.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_protocol/test_generated.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_protocol/test_header.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_protocol/test_protocol_generator.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_theme/__init__.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_theme/conftest.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_theme/test_apply_theme.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_theme/test_canvas.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_theme/test_generators.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_theme/test_library.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_theme/test_theme.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.1.0}/tests/test_utils.py +0 -0
|
@@ -35,7 +35,7 @@ jobs:
|
|
|
35
35
|
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
|
36
36
|
|
|
37
37
|
- name: Set up Python
|
|
38
|
-
uses: actions/setup-python@
|
|
38
|
+
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
|
|
39
39
|
with:
|
|
40
40
|
python-version: ${{ env.PYTHON_VERSION }}
|
|
41
41
|
|
|
@@ -62,6 +62,47 @@ jobs:
|
|
|
62
62
|
uv pip install "bandit[toml]"
|
|
63
63
|
uv run bandit -c pyproject.toml -r src/
|
|
64
64
|
|
|
65
|
+
# Verify auto-generated files are up-to-date
|
|
66
|
+
generated-files:
|
|
67
|
+
name: Verify Generated Files
|
|
68
|
+
needs: quality
|
|
69
|
+
runs-on: ubuntu-latest
|
|
70
|
+
steps:
|
|
71
|
+
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
|
72
|
+
|
|
73
|
+
- name: Set up Python
|
|
74
|
+
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
|
|
75
|
+
with:
|
|
76
|
+
python-version: ${{ env.PYTHON_VERSION }}
|
|
77
|
+
|
|
78
|
+
- name: Install uv
|
|
79
|
+
uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b # v7
|
|
80
|
+
with:
|
|
81
|
+
version: ${{ env.UV_VERSION }}
|
|
82
|
+
python-version: ${{ env.PYTHON_VERSION }}
|
|
83
|
+
|
|
84
|
+
- name: Install dependencies
|
|
85
|
+
run: uv sync --frozen
|
|
86
|
+
|
|
87
|
+
- name: Regenerate protocol files
|
|
88
|
+
run: uv run python -m lifx.protocol.generator
|
|
89
|
+
|
|
90
|
+
- name: Regenerate product registry
|
|
91
|
+
run: uv run python -m lifx.products.generator
|
|
92
|
+
|
|
93
|
+
- name: Format and lint generated files
|
|
94
|
+
run: |
|
|
95
|
+
uv run ruff format src/lifx/protocol/packets.py src/lifx/protocol/protocol_types.py src/lifx/products/registry.py
|
|
96
|
+
uv run ruff check --fix src/lifx/protocol/packets.py src/lifx/protocol/protocol_types.py src/lifx/products/registry.py
|
|
97
|
+
|
|
98
|
+
- name: Check for differences
|
|
99
|
+
run: |
|
|
100
|
+
if ! git diff --exit-code src/lifx/protocol/packets.py src/lifx/protocol/protocol_types.py src/lifx/products/registry.py; then
|
|
101
|
+
echo "::error::Generated files do not match committed versions. See diff above for details."
|
|
102
|
+
exit 1
|
|
103
|
+
fi
|
|
104
|
+
echo "All generated files are up-to-date."
|
|
105
|
+
|
|
65
106
|
# Testing matrix across Python versions and platforms
|
|
66
107
|
test:
|
|
67
108
|
name: Test (Python ${{ matrix.python-version }} on ${{ matrix.os }})
|
|
@@ -72,12 +113,12 @@ jobs:
|
|
|
72
113
|
fail-fast: false
|
|
73
114
|
matrix:
|
|
74
115
|
os: [ubuntu-latest, macos-latest, windows-latest]
|
|
75
|
-
python-version: ['3.11', '3.12', '3.13', '3.14']
|
|
116
|
+
python-version: ['3.10', '3.11', '3.12', '3.13', '3.14']
|
|
76
117
|
steps:
|
|
77
118
|
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
|
78
119
|
|
|
79
120
|
- name: Set up Python ${{ matrix.python-version }}
|
|
80
|
-
uses: actions/setup-python@
|
|
121
|
+
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
|
|
81
122
|
with:
|
|
82
123
|
python-version: ${{ matrix.python-version }}
|
|
83
124
|
|
|
@@ -129,7 +170,7 @@ jobs:
|
|
|
129
170
|
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
|
130
171
|
|
|
131
172
|
- name: Set up Python
|
|
132
|
-
uses: actions/setup-python@
|
|
173
|
+
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
|
|
133
174
|
with:
|
|
134
175
|
python-version: ${{ env.PYTHON_VERSION }}
|
|
135
176
|
|
|
@@ -167,6 +208,7 @@ jobs:
|
|
|
167
208
|
release:
|
|
168
209
|
needs:
|
|
169
210
|
- quality
|
|
211
|
+
- generated-files
|
|
170
212
|
- test
|
|
171
213
|
# Run release if not cancelled AND (manual dispatch OR quality passed with tests passed/skipped)
|
|
172
214
|
if: ${{ !cancelled() && (github.event_name == 'workflow_dispatch' || (needs.quality.result == 'success' && (needs.test.result == 'success' || needs.test.result == 'skipped'))) }}
|
|
@@ -193,7 +235,7 @@ jobs:
|
|
|
193
235
|
ssh-key: ${{ secrets.DEPLOY_KEY }}
|
|
194
236
|
|
|
195
237
|
- name: Set up Python
|
|
196
|
-
uses: actions/setup-python@
|
|
238
|
+
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
|
|
197
239
|
with:
|
|
198
240
|
python-version: ${{ env.PYTHON_VERSION }}
|
|
199
241
|
|
|
@@ -30,7 +30,7 @@ jobs:
|
|
|
30
30
|
fetch-depth: 0 # Fetch all history for git-revision-date-localized
|
|
31
31
|
|
|
32
32
|
- name: Set up Python
|
|
33
|
-
uses: actions/setup-python@
|
|
33
|
+
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
|
|
34
34
|
with:
|
|
35
35
|
python-version: ${{ env.PYTHON_VERSION }}
|
|
36
36
|
|
|
@@ -64,7 +64,7 @@ jobs:
|
|
|
64
64
|
fetch-depth: 0
|
|
65
65
|
|
|
66
66
|
- name: Set up Python
|
|
67
|
-
uses: actions/setup-python@
|
|
67
|
+
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
|
|
68
68
|
with:
|
|
69
69
|
python-version: ${{ env.PYTHON_VERSION }}
|
|
70
70
|
|
|
@@ -92,7 +92,7 @@ jobs:
|
|
|
92
92
|
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
|
93
93
|
|
|
94
94
|
- name: Set up Python
|
|
95
|
-
uses: actions/setup-python@
|
|
95
|
+
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
|
|
96
96
|
with:
|
|
97
97
|
python-version: ${{ env.PYTHON_VERSION }}
|
|
98
98
|
|
|
@@ -149,7 +149,17 @@ uv run mkdocs gh-deploy
|
|
|
149
149
|
- `DeviceGroup`: Batch operations (set_power, set_color, etc.)
|
|
150
150
|
- `LocationGrouping` / `GroupGrouping`: Organizational structures for location/group-based grouping
|
|
151
151
|
|
|
152
|
-
5. **
|
|
152
|
+
5. **Animation Layer** (`src/lifx/animation/`)
|
|
153
|
+
|
|
154
|
+
- `animator.py`: High-level `Animator` class with direct UDP sending
|
|
155
|
+
- `framebuffer.py`: Multi-tile canvas mapping and orientation correction
|
|
156
|
+
- `packets.py`: Prebaked packet templates (`MatrixPacketGenerator`, `MultiZonePacketGenerator`)
|
|
157
|
+
- `orientation.py`: Tile orientation remapping with LRU-cached lookup tables
|
|
158
|
+
- Optimized for high-frequency frame delivery (30+ FPS) for real-time effects
|
|
159
|
+
- Uses protocol-ready uint16 HSBK values (no conversion overhead)
|
|
160
|
+
- Multi-tile canvas support using `user_x`/`user_y` tile positions
|
|
161
|
+
|
|
162
|
+
6. **Utilities**
|
|
153
163
|
|
|
154
164
|
- `color.py`: `HSBK` class with RGB conversion, `Colors` presets
|
|
155
165
|
- `const.py`: Critical constants (network settings, UUIDs, official URLs)
|
|
@@ -481,6 +491,80 @@ for frame in animation_frames:
|
|
|
481
491
|
|
|
482
492
|
**Note:** `MatrixLight.set64()` is already fire-and-forget by default.
|
|
483
493
|
|
|
494
|
+
### Animation Module (High-Frequency Frame Delivery)
|
|
495
|
+
|
|
496
|
+
For real-time effects and applications that need to push color data at 30+ FPS, use the animation module:
|
|
497
|
+
|
|
498
|
+
```python
|
|
499
|
+
from lifx import Animator, MatrixLight
|
|
500
|
+
|
|
501
|
+
async with await MatrixLight.from_ip("192.168.1.100") as device:
|
|
502
|
+
# Create animator for matrix device
|
|
503
|
+
animator = await Animator.for_matrix(device)
|
|
504
|
+
|
|
505
|
+
# Device connection closed - animator sends via direct UDP
|
|
506
|
+
while running:
|
|
507
|
+
# Generate HSBK frame (protocol-ready uint16 values)
|
|
508
|
+
# H/S/B: 0-65535, K: 1500-9000
|
|
509
|
+
hsbk_frame = [(65535, 65535, 65535, 3500)] * animator.pixel_count
|
|
510
|
+
|
|
511
|
+
# send_frame() is synchronous for speed
|
|
512
|
+
stats = animator.send_frame(hsbk_frame)
|
|
513
|
+
print(f"Sent {stats.packets_sent} packets")
|
|
514
|
+
|
|
515
|
+
await asyncio.sleep(1 / 30) # 30 FPS
|
|
516
|
+
|
|
517
|
+
animator.close()
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
**Key Features:**
|
|
521
|
+
- **Direct UDP**: Bypasses connection layer for maximum throughput
|
|
522
|
+
- **Prebaked packets**: Templates created once, only colors updated per frame
|
|
523
|
+
- **Multi-tile canvas**: Unified coordinate space for multi-tile devices (e.g., 5-tile LIFX Tile)
|
|
524
|
+
- **Tile orientation**: Automatic pixel remapping for rotated tiles
|
|
525
|
+
|
|
526
|
+
**Multi-Tile Canvas:**
|
|
527
|
+
|
|
528
|
+
For devices with multiple tiles, the animator creates a unified canvas based on tile positions:
|
|
529
|
+
|
|
530
|
+
```python
|
|
531
|
+
async with await MatrixLight.from_ip("192.168.1.100") as device:
|
|
532
|
+
animator = await Animator.for_matrix(device)
|
|
533
|
+
|
|
534
|
+
# For 5 horizontal tiles: canvas is 40x8 (320 pixels)
|
|
535
|
+
print(f"Canvas: {animator.canvas_width}x{animator.canvas_height}")
|
|
536
|
+
|
|
537
|
+
# Generate frame for entire canvas (row-major order)
|
|
538
|
+
frame = []
|
|
539
|
+
for y in range(animator.canvas_height):
|
|
540
|
+
for x in range(animator.canvas_width):
|
|
541
|
+
hue = int(x / animator.canvas_width * 65535) # Rainbow across all tiles
|
|
542
|
+
frame.append((hue, 65535, 65535, 3500))
|
|
543
|
+
|
|
544
|
+
animator.send_frame(frame)
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
**HSBK Format (Protocol-Ready):**
|
|
548
|
+
```python
|
|
549
|
+
# (hue, saturation, brightness, kelvin)
|
|
550
|
+
# H/S/B: 0-65535, K: 1500-9000
|
|
551
|
+
red = (0, 65535, 65535, 3500) # Full red
|
|
552
|
+
blue = (43690, 65535, 65535, 3500) # Full blue (240/360 * 65535)
|
|
553
|
+
white = (0, 0, 65535, 5500) # Daylight white
|
|
554
|
+
off = (0, 0, 0, 3500) # Off (black)
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
**For MultiZone devices (strips/beams):**
|
|
558
|
+
```python
|
|
559
|
+
from lifx import Animator, MultiZoneLight
|
|
560
|
+
|
|
561
|
+
async with await MultiZoneLight.from_ip("192.168.1.100") as device:
|
|
562
|
+
animator = await Animator.for_multizone(device)
|
|
563
|
+
|
|
564
|
+
# Same API as matrix
|
|
565
|
+
stats = animator.send_frame(hsbk_frame)
|
|
566
|
+
```
|
|
567
|
+
|
|
484
568
|
### Packet Flow
|
|
485
569
|
|
|
486
570
|
1. Create packet instance (e.g., `LightSetColor`)
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
# Animation Module
|
|
2
|
+
|
|
3
|
+
The animation module provides efficient high-frequency frame delivery for LIFX devices, optimized
|
|
4
|
+
for real-time effects and applications that need to push color data at 30+ FPS.
|
|
5
|
+
|
|
6
|
+
## Overview
|
|
7
|
+
|
|
8
|
+
The animation system uses a streamlined architecture optimized for speed:
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
Application Frame -> FrameBuffer -> PacketGenerator -> Direct UDP
|
|
12
|
+
(canvas map) (prebaked packets) (fire-and-forget)
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Key features:
|
|
16
|
+
|
|
17
|
+
- **Direct UDP**: Bypasses connection layer for maximum throughput
|
|
18
|
+
- **Prebaked packets**: Templates created once, only colors updated per frame
|
|
19
|
+
- **Multi-tile canvas**: Unified coordinate space for multi-tile devices
|
|
20
|
+
- **Tile orientation**: Automatic pixel remapping for rotated tiles
|
|
21
|
+
- **Synchronous sending**: `send_frame()` is synchronous for minimum overhead
|
|
22
|
+
|
|
23
|
+
## Quick Start
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
import asyncio
|
|
27
|
+
from lifx import Animator, MatrixLight
|
|
28
|
+
|
|
29
|
+
async def main():
|
|
30
|
+
async with await MatrixLight.from_ip("192.168.1.100") as device:
|
|
31
|
+
# Create animator for matrix device
|
|
32
|
+
animator = await Animator.for_matrix(device)
|
|
33
|
+
|
|
34
|
+
# Device connection closed - animator sends via direct UDP
|
|
35
|
+
try:
|
|
36
|
+
while True:
|
|
37
|
+
# Generate HSBK frame (protocol-ready uint16 values)
|
|
38
|
+
# H/S/B: 0-65535, K: 1500-9000
|
|
39
|
+
hsbk_frame = [(65535, 65535, 65535, 3500)] * animator.pixel_count
|
|
40
|
+
|
|
41
|
+
# send_frame() is synchronous for speed
|
|
42
|
+
stats = animator.send_frame(hsbk_frame)
|
|
43
|
+
print(f"Sent {stats.packets_sent} packets in {stats.total_time_ms:.2f}ms")
|
|
44
|
+
|
|
45
|
+
await asyncio.sleep(1 / 30) # 30 FPS
|
|
46
|
+
finally:
|
|
47
|
+
animator.close()
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Multi-Tile Canvas
|
|
51
|
+
|
|
52
|
+
For devices with multiple tiles (like the original 5-tile LIFX Tile), the animator creates
|
|
53
|
+
a unified canvas based on tile positions (`user_x`, `user_y`). Animations span all tiles
|
|
54
|
+
as one continuous image.
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
async with await MatrixLight.from_ip("192.168.1.100") as device:
|
|
58
|
+
animator = await Animator.for_matrix(device)
|
|
59
|
+
|
|
60
|
+
# Check canvas dimensions
|
|
61
|
+
print(f"Canvas: {animator.canvas_width}x{animator.canvas_height}")
|
|
62
|
+
# For 5 horizontal tiles: "Canvas: 40x8"
|
|
63
|
+
|
|
64
|
+
# Generate frame for entire canvas (row-major order)
|
|
65
|
+
frame = []
|
|
66
|
+
for y in range(animator.canvas_height):
|
|
67
|
+
for x in range(animator.canvas_width):
|
|
68
|
+
hue = int(x / animator.canvas_width * 65535) # Gradient across all tiles
|
|
69
|
+
frame.append((hue, 65535, 65535, 3500))
|
|
70
|
+
|
|
71
|
+
animator.send_frame(frame)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## HSBK Format
|
|
75
|
+
|
|
76
|
+
All color data uses protocol-ready uint16 values:
|
|
77
|
+
|
|
78
|
+
| Component | Range | Description |
|
|
79
|
+
|-----------|-------|-------------|
|
|
80
|
+
| Hue | 0-65535 | Maps to 0-360 degrees |
|
|
81
|
+
| Saturation | 0-65535 | Maps to 0.0-1.0 |
|
|
82
|
+
| Brightness | 0-65535 | Maps to 0.0-1.0 |
|
|
83
|
+
| Kelvin | 1500-9000 | Color temperature |
|
|
84
|
+
|
|
85
|
+
This design pushes conversion work to the caller (e.g. using NumPy) for better performance.
|
|
86
|
+
The `lifx-async` library remains dependency-free.
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
# Red at full brightness
|
|
90
|
+
red = (0, 65535, 65535, 3500)
|
|
91
|
+
|
|
92
|
+
# 50% brightness warm white
|
|
93
|
+
warm_white = (0, 0, 32768, 2700)
|
|
94
|
+
|
|
95
|
+
# Convert from user-friendly values
|
|
96
|
+
def to_protocol_hsbk(
|
|
97
|
+
hue: float, sat: float, bright: float, kelvin: int
|
|
98
|
+
) -> tuple[int, int, int, int]:
|
|
99
|
+
"""Convert user-friendly values to protocol format."""
|
|
100
|
+
return (
|
|
101
|
+
int(hue / 360 * 65535),
|
|
102
|
+
int(sat * 65535),
|
|
103
|
+
int(bright * 65535),
|
|
104
|
+
kelvin,
|
|
105
|
+
)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Animator
|
|
109
|
+
|
|
110
|
+
High-level class integrating all animation components.
|
|
111
|
+
|
|
112
|
+
::: lifx.animation.animator.Animator
|
|
113
|
+
options:
|
|
114
|
+
show_root_heading: true
|
|
115
|
+
heading_level: 3
|
|
116
|
+
members_order: source
|
|
117
|
+
show_if_no_docstring: false
|
|
118
|
+
filters:
|
|
119
|
+
- "!^_"
|
|
120
|
+
|
|
121
|
+
### AnimatorStats
|
|
122
|
+
|
|
123
|
+
Statistics returned by `Animator.send_frame()`.
|
|
124
|
+
|
|
125
|
+
::: lifx.animation.animator.AnimatorStats
|
|
126
|
+
options:
|
|
127
|
+
show_root_heading: true
|
|
128
|
+
heading_level: 4
|
|
129
|
+
members_order: source
|
|
130
|
+
show_if_no_docstring: false
|
|
131
|
+
|
|
132
|
+
## FrameBuffer
|
|
133
|
+
|
|
134
|
+
Canvas mapping and orientation handling for matrix devices.
|
|
135
|
+
|
|
136
|
+
::: lifx.animation.framebuffer.FrameBuffer
|
|
137
|
+
options:
|
|
138
|
+
show_root_heading: true
|
|
139
|
+
heading_level: 3
|
|
140
|
+
members_order: source
|
|
141
|
+
show_if_no_docstring: false
|
|
142
|
+
filters:
|
|
143
|
+
- "!^_"
|
|
144
|
+
|
|
145
|
+
### TileRegion
|
|
146
|
+
|
|
147
|
+
Represents a tile's region within the canvas.
|
|
148
|
+
|
|
149
|
+
::: lifx.animation.framebuffer.TileRegion
|
|
150
|
+
options:
|
|
151
|
+
show_root_heading: true
|
|
152
|
+
heading_level: 4
|
|
153
|
+
members_order: source
|
|
154
|
+
show_if_no_docstring: false
|
|
155
|
+
|
|
156
|
+
## Packet Generators
|
|
157
|
+
|
|
158
|
+
Device-specific packet generation with prebaked templates.
|
|
159
|
+
|
|
160
|
+
### PacketGenerator (Base)
|
|
161
|
+
|
|
162
|
+
::: lifx.animation.packets.PacketGenerator
|
|
163
|
+
options:
|
|
164
|
+
show_root_heading: true
|
|
165
|
+
heading_level: 4
|
|
166
|
+
members_order: source
|
|
167
|
+
show_if_no_docstring: false
|
|
168
|
+
|
|
169
|
+
### PacketTemplate
|
|
170
|
+
|
|
171
|
+
Prebaked packet template for zero-allocation frame updates.
|
|
172
|
+
|
|
173
|
+
::: lifx.animation.packets.PacketTemplate
|
|
174
|
+
options:
|
|
175
|
+
show_root_heading: true
|
|
176
|
+
heading_level: 4
|
|
177
|
+
members_order: source
|
|
178
|
+
show_if_no_docstring: false
|
|
179
|
+
|
|
180
|
+
### MatrixPacketGenerator
|
|
181
|
+
|
|
182
|
+
Generates Set64 packets for MatrixLight devices.
|
|
183
|
+
|
|
184
|
+
::: lifx.animation.packets.MatrixPacketGenerator
|
|
185
|
+
options:
|
|
186
|
+
show_root_heading: true
|
|
187
|
+
heading_level: 4
|
|
188
|
+
members_order: source
|
|
189
|
+
show_if_no_docstring: false
|
|
190
|
+
filters:
|
|
191
|
+
- "!^_"
|
|
192
|
+
|
|
193
|
+
### MultiZonePacketGenerator
|
|
194
|
+
|
|
195
|
+
Generates SetExtendedColorZones packets for MultiZoneLight devices.
|
|
196
|
+
|
|
197
|
+
::: lifx.animation.packets.MultiZonePacketGenerator
|
|
198
|
+
options:
|
|
199
|
+
show_root_heading: true
|
|
200
|
+
heading_level: 4
|
|
201
|
+
members_order: source
|
|
202
|
+
show_if_no_docstring: false
|
|
203
|
+
filters:
|
|
204
|
+
- "!^_"
|
|
205
|
+
|
|
206
|
+
## Tile Orientation
|
|
207
|
+
|
|
208
|
+
Pixel remapping for rotated tiles.
|
|
209
|
+
|
|
210
|
+
### Orientation Enum
|
|
211
|
+
|
|
212
|
+
::: lifx.animation.orientation.Orientation
|
|
213
|
+
options:
|
|
214
|
+
show_root_heading: true
|
|
215
|
+
heading_level: 4
|
|
216
|
+
members_order: source
|
|
217
|
+
show_if_no_docstring: false
|
|
218
|
+
|
|
219
|
+
### build_orientation_lut
|
|
220
|
+
|
|
221
|
+
::: lifx.animation.orientation.build_orientation_lut
|
|
222
|
+
options:
|
|
223
|
+
show_root_heading: true
|
|
224
|
+
heading_level: 4
|
|
225
|
+
|
|
226
|
+
## Examples
|
|
227
|
+
|
|
228
|
+
### Matrix Animation (Single Tile)
|
|
229
|
+
|
|
230
|
+
```python
|
|
231
|
+
import asyncio
|
|
232
|
+
from lifx import Animator, MatrixLight
|
|
233
|
+
|
|
234
|
+
async def rainbow_animation():
|
|
235
|
+
async with await MatrixLight.from_ip("192.168.1.100") as device:
|
|
236
|
+
animator = await Animator.for_matrix(device)
|
|
237
|
+
|
|
238
|
+
hue_offset = 0
|
|
239
|
+
try:
|
|
240
|
+
while True:
|
|
241
|
+
# Generate rainbow gradient
|
|
242
|
+
frame = []
|
|
243
|
+
for i in range(animator.pixel_count):
|
|
244
|
+
hue = (hue_offset + i * 1000) % 65536
|
|
245
|
+
frame.append((hue, 65535, 32768, 3500))
|
|
246
|
+
|
|
247
|
+
stats = animator.send_frame(frame)
|
|
248
|
+
print(f"Sent {stats.packets_sent} packets")
|
|
249
|
+
|
|
250
|
+
hue_offset = (hue_offset + 500) % 65536
|
|
251
|
+
await asyncio.sleep(1 / 30) # 30 FPS
|
|
252
|
+
finally:
|
|
253
|
+
animator.close()
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
### Multi-Tile Animation (LIFX Tile with 5 tiles)
|
|
257
|
+
|
|
258
|
+
```python
|
|
259
|
+
import asyncio
|
|
260
|
+
import math
|
|
261
|
+
from lifx import Animator, MatrixLight
|
|
262
|
+
|
|
263
|
+
async def multi_tile_wave():
|
|
264
|
+
async with await MatrixLight.from_ip("192.168.1.100") as device:
|
|
265
|
+
animator = await Animator.for_matrix(device)
|
|
266
|
+
|
|
267
|
+
# Canvas spans all tiles (e.g., 40x8 for 5 horizontal tiles)
|
|
268
|
+
width = animator.canvas_width
|
|
269
|
+
height = animator.canvas_height
|
|
270
|
+
print(f"Canvas: {width}x{height}")
|
|
271
|
+
|
|
272
|
+
hue_offset = 0
|
|
273
|
+
try:
|
|
274
|
+
while True:
|
|
275
|
+
frame = []
|
|
276
|
+
for y in range(height):
|
|
277
|
+
for x in range(width):
|
|
278
|
+
# Wave that flows across all tiles
|
|
279
|
+
pos = x + y * 0.5 # Diagonal wave
|
|
280
|
+
hue = int((pos / width) * 65535 + hue_offset) % 65536
|
|
281
|
+
frame.append((hue, 65535, 65535, 3500))
|
|
282
|
+
|
|
283
|
+
animator.send_frame(frame)
|
|
284
|
+
hue_offset = (hue_offset + 1000) % 65536
|
|
285
|
+
await asyncio.sleep(1 / 30)
|
|
286
|
+
finally:
|
|
287
|
+
animator.close()
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
### MultiZone Animation
|
|
291
|
+
|
|
292
|
+
```python
|
|
293
|
+
import asyncio
|
|
294
|
+
from lifx import Animator, MultiZoneLight
|
|
295
|
+
|
|
296
|
+
async def chase_animation():
|
|
297
|
+
async with await MultiZoneLight.from_ip("192.168.1.100") as device:
|
|
298
|
+
animator = await Animator.for_multizone(device)
|
|
299
|
+
|
|
300
|
+
position = 0
|
|
301
|
+
try:
|
|
302
|
+
while True:
|
|
303
|
+
# Generate chase pattern
|
|
304
|
+
frame = []
|
|
305
|
+
for i in range(animator.pixel_count):
|
|
306
|
+
if i == position:
|
|
307
|
+
frame.append((0, 65535, 65535, 3500)) # Red
|
|
308
|
+
else:
|
|
309
|
+
frame.append((0, 0, 0, 3500)) # Off
|
|
310
|
+
|
|
311
|
+
animator.send_frame(frame)
|
|
312
|
+
|
|
313
|
+
position = (position + 1) % animator.pixel_count
|
|
314
|
+
await asyncio.sleep(1 / 20) # 20 FPS
|
|
315
|
+
finally:
|
|
316
|
+
animator.close()
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
## Performance Characteristics
|
|
320
|
+
|
|
321
|
+
### Direct UDP Delivery
|
|
322
|
+
|
|
323
|
+
The animation module bypasses the connection layer entirely:
|
|
324
|
+
|
|
325
|
+
- No ACKs, no waiting, no retries
|
|
326
|
+
- Packets sent via raw UDP socket
|
|
327
|
+
- Maximum throughput for real-time effects
|
|
328
|
+
- Some packet loss is acceptable (visual artifacts are brief)
|
|
329
|
+
|
|
330
|
+
### Prebaked Packet Templates
|
|
331
|
+
|
|
332
|
+
Packets are constructed once at initialization:
|
|
333
|
+
|
|
334
|
+
- Header and payload structure prebaked as `bytearray`
|
|
335
|
+
- Per-frame: only color data and sequence number updated
|
|
336
|
+
- Zero object allocation in the hot path
|
|
337
|
+
- Sequence number wraps at 256 (uint8)
|
|
338
|
+
|
|
339
|
+
### Multi-Tile Canvas Mapping
|
|
340
|
+
|
|
341
|
+
For devices with multiple tiles:
|
|
342
|
+
|
|
343
|
+
- Tile positions read from device (`user_x`, `user_y`)
|
|
344
|
+
- Canvas bounds calculated from all tile positions
|
|
345
|
+
- Input frame interpreted as 2D row-major canvas
|
|
346
|
+
- Each tile extracts its region based on position
|
|
347
|
+
- Orientation correction applied per-tile
|
|
348
|
+
|
|
349
|
+
### Typical Performance
|
|
350
|
+
|
|
351
|
+
| Device Type | Pixels | Packets/Frame | Send Time |
|
|
352
|
+
|-------------|--------|---------------|-----------|
|
|
353
|
+
| Single tile (8x8) | 64 | 1 | <0.5ms |
|
|
354
|
+
| 5-tile chain | 320 | 5 | <1ms |
|
|
355
|
+
| Large Ceiling (16x8) | 128 | 3 | <1ms |
|
|
356
|
+
| MultiZone (82 zones) | 82 | 1 | <0.5ms |
|
|
@@ -11,6 +11,12 @@ lifx/
|
|
|
11
11
|
├── color.py # Color utilities (HSBK, Colors)
|
|
12
12
|
├── const.py # Network constants and URLs
|
|
13
13
|
├── exceptions.py # Exception hierarchy
|
|
14
|
+
├── animation/ # Animation module for high-frequency frame delivery
|
|
15
|
+
│ ├── __init__.py # Public API exports
|
|
16
|
+
│ ├── animator.py # High-level Animator class with direct UDP
|
|
17
|
+
│ ├── framebuffer.py # Multi-tile canvas mapping and orientation
|
|
18
|
+
│ ├── packets.py # Prebaked packet templates
|
|
19
|
+
│ └── orientation.py # Tile orientation remapping
|
|
14
20
|
├── devices/ # Device classes
|
|
15
21
|
│ ├── base.py # Base Device class
|
|
16
22
|
│ ├── light.py # Light device (color control)
|
|
@@ -73,6 +79,14 @@ Work with colors:
|
|
|
73
79
|
- [`HSBK`](colors.md#lifx.color.HSBK) - Color representation
|
|
74
80
|
- [`Colors`](colors.md#lifx.color.Colors) - Built-in presets
|
|
75
81
|
|
|
82
|
+
### Animation
|
|
83
|
+
|
|
84
|
+
High-frequency frame delivery for real-time effects:
|
|
85
|
+
|
|
86
|
+
- [`Animator`](animation.md#lifx.animation.animator.Animator) - High-level animation interface with direct UDP
|
|
87
|
+
- [`FrameBuffer`](animation.md#lifx.animation.framebuffer.FrameBuffer) - Multi-tile canvas mapping
|
|
88
|
+
- [`PacketTemplate`](animation.md#lifx.animation.packets.PacketTemplate) - Prebaked packet templates
|
|
89
|
+
|
|
76
90
|
### Network Layer
|
|
77
91
|
|
|
78
92
|
Low-level network operations:
|
|
@@ -195,6 +209,14 @@ async def set_custom_color(light: Light, hue: float) -> None:
|
|
|
195
209
|
|
|
196
210
|
[:octicons-arrow-right-24: Colors](colors.md)
|
|
197
211
|
|
|
212
|
+
- :material-animation:{ .lg .middle } __Animation__
|
|
213
|
+
|
|
214
|
+
______________________________________________________________________
|
|
215
|
+
|
|
216
|
+
High-frequency frame delivery for real-time effects
|
|
217
|
+
|
|
218
|
+
[:octicons-arrow-right-24: Animation](animation.md)
|
|
219
|
+
|
|
198
220
|
- :material-network:{ .lg .middle } __Network Layer__
|
|
199
221
|
|
|
200
222
|
______________________________________________________________________
|
|
@@ -30,19 +30,6 @@ The LIFX protocol header structure (36 bytes).
|
|
|
30
30
|
members_order: source
|
|
31
31
|
show_if_no_docstring: false
|
|
32
32
|
|
|
33
|
-
## Serializer
|
|
34
|
-
|
|
35
|
-
Binary serialization and deserialization utilities.
|
|
36
|
-
|
|
37
|
-
::: lifx.protocol.serializer.FieldSerializer
|
|
38
|
-
options:
|
|
39
|
-
show_root_heading: true
|
|
40
|
-
heading_level: 3
|
|
41
|
-
members_order: source
|
|
42
|
-
show_if_no_docstring: false
|
|
43
|
-
filters:
|
|
44
|
-
- "!^_"
|
|
45
|
-
|
|
46
33
|
## Protocol Types
|
|
47
34
|
|
|
48
35
|
Common protocol type definitions and enums.
|
|
@@ -241,7 +228,6 @@ async def main():
|
|
|
241
228
|
|
|
242
229
|
```python
|
|
243
230
|
from lifx.protocol.packets import DeviceSetLabel
|
|
244
|
-
from lifx.protocol.serializer import Serializer
|
|
245
231
|
|
|
246
232
|
# Create packet
|
|
247
233
|
packet = DeviceSetLabel(label=b"Kitchen Light\0" + b"\0" * 19)
|
|
@@ -2,6 +2,35 @@
|
|
|
2
2
|
|
|
3
3
|
<!-- version list -->
|
|
4
4
|
|
|
5
|
+
## v5.1.0 (2026-01-24)
|
|
6
|
+
|
|
7
|
+
### Bug Fixes
|
|
8
|
+
|
|
9
|
+
- **animation**: Load capabilities before checking has_chain
|
|
10
|
+
([`69e50b3`](https://github.com/Djelibeybi/lifx-async/commit/69e50b356ed22ad1679a823444db9fa7c19626b7))
|
|
11
|
+
|
|
12
|
+
### Documentation
|
|
13
|
+
|
|
14
|
+
- Add DeviceGroup usage example
|
|
15
|
+
([`9f85854`](https://github.com/Djelibeybi/lifx-async/commit/9f858543f745140c154128c63006047fccdbd823))
|
|
16
|
+
|
|
17
|
+
- Remove reference to deleted FieldSerializer class
|
|
18
|
+
([`5db0262`](https://github.com/Djelibeybi/lifx-async/commit/5db0262b593121be2de00bba44d522e11e449845))
|
|
19
|
+
|
|
20
|
+
### Features
|
|
21
|
+
|
|
22
|
+
- **animation**: Add high-performance animation module
|
|
23
|
+
([`afc8063`](https://github.com/Djelibeybi/lifx-async/commit/afc8063e6d863e118377facab30a7d3035b1ded5))
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
## v5.0.1 (2026-01-14)
|
|
27
|
+
|
|
28
|
+
### Bug Fixes
|
|
29
|
+
|
|
30
|
+
- Handle asyncio.TimeoutError on Python 3.10
|
|
31
|
+
([`4438bc4`](https://github.com/Djelibeybi/lifx-async/commit/4438bc45f19f477b585c6af8cf8cbaf5e9341d14))
|
|
32
|
+
|
|
33
|
+
|
|
5
34
|
## v5.0.0 (2026-01-12)
|
|
6
35
|
|
|
7
36
|
### Features
|