lifx-async 5.1.0__tar.gz → 5.1.1__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.1.0 → lifx_async-5.1.1}/.github/workflows/ci.yml +10 -10
- {lifx_async-5.1.0 → lifx_async-5.1.1}/.github/workflows/docs.yml +6 -6
- {lifx_async-5.1.0 → lifx_async-5.1.1}/PKG-INFO +1 -1
- {lifx_async-5.1.0 → lifx_async-5.1.1}/docs/changelog.md +11 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/examples/15_animation.py +288 -14
- {lifx_async-5.1.0 → lifx_async-5.1.1}/examples/16_animation_numpy.py +319 -14
- {lifx_async-5.1.0 → lifx_async-5.1.1}/pyproject.toml +1 -1
- {lifx_async-5.1.0 → lifx_async-5.1.1}/src/lifx/animation/packets.py +24 -12
- {lifx_async-5.1.0 → lifx_async-5.1.1}/src/lifx/devices/base.py +80 -33
- {lifx_async-5.1.0 → lifx_async-5.1.1}/src/lifx/devices/light.py +45 -22
- {lifx_async-5.1.0 → lifx_async-5.1.1}/src/lifx/network/connection.py +46 -13
- {lifx_async-5.1.0 → lifx_async-5.1.1}/src/lifx/protocol/base.py +48 -28
- {lifx_async-5.1.0 → lifx_async-5.1.1}/src/lifx/protocol/header.py +3 -4
- {lifx_async-5.1.0 → lifx_async-5.1.1}/tests/test_animation/test_packets.py +4 -2
- {lifx_async-5.1.0 → lifx_async-5.1.1}/tests/test_devices/test_state_light.py +140 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/tests/test_devices/test_state_management.py +235 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/tests/test_network/test_connection.py +44 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/uv.lock +1 -1
- {lifx_async-5.1.0 → lifx_async-5.1.1}/.claude/settings.json +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/.github/dependabot.yml +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/.github/labeler.yml +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/.github/workflows/pr-automation.yml +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/.gitignore +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/.pre-commit-config.yaml +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/CLAUDE.md +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/LICENSE +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/README.md +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/context7.json +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/docs/api/animation.md +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/docs/api/colors.md +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/docs/api/devices.md +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/docs/api/effects.md +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/docs/api/exceptions.md +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/docs/api/high-level.md +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/docs/api/index.md +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/docs/api/network.md +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/docs/api/protocol.md +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/docs/api/themes.md +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/docs/architecture/effects-architecture.md +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/docs/architecture/overview.md +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/docs/faq.md +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/docs/getting-started/effects.md +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/docs/getting-started/installation.md +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/docs/getting-started/quickstart.md +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/docs/getting-started/themes.md +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/docs/index.md +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/docs/migration/effect-api-changes.md +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/docs/stylesheets/extra.css +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/docs/user-guide/advanced-usage.md +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/docs/user-guide/animation.md +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/docs/user-guide/ceiling-lights.md +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/docs/user-guide/effects-custom.md +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/docs/user-guide/effects-troubleshooting.md +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/docs/user-guide/protocol-deep-dive.md +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/docs/user-guide/themes.md +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/docs/user-guide/troubleshooting.md +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/examples/01_simple_discovery.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/examples/02_simple_control.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/examples/03_waveforms.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/examples/04_logging.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/examples/05_device_groups.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/examples/06_pulse_effect.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/examples/07_colorloop_effect.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/examples/08_custom_effect.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/examples/09_background_effect.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/examples/10_find_specific_devices.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/examples/11_matrix_basic.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/examples/12_matrix_effects.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/examples/13_matrix_large.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/examples/14_mdns_discovery.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/mkdocs.yml +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/renovate.json +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/scripts/mdns_probe.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/scripts/test_multiversion.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/src/lifx/__init__.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/src/lifx/animation/__init__.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/src/lifx/animation/animator.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/src/lifx/animation/framebuffer.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/src/lifx/animation/orientation.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/src/lifx/api.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/src/lifx/color.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/src/lifx/const.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/src/lifx/devices/__init__.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/src/lifx/devices/ceiling.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/src/lifx/devices/hev.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/src/lifx/devices/infrared.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/src/lifx/devices/matrix.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/src/lifx/devices/multizone.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/src/lifx/effects/__init__.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/src/lifx/effects/base.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/src/lifx/effects/colorloop.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/src/lifx/effects/conductor.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/src/lifx/effects/const.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/src/lifx/effects/models.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/src/lifx/effects/pulse.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/src/lifx/effects/state_manager.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/src/lifx/exceptions.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/src/lifx/network/__init__.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/src/lifx/network/discovery.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/src/lifx/network/mdns/__init__.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/src/lifx/network/mdns/discovery.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/src/lifx/network/mdns/dns.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/src/lifx/network/mdns/transport.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/src/lifx/network/mdns/types.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/src/lifx/network/message.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/src/lifx/network/transport.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/src/lifx/network/utils.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/src/lifx/products/__init__.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/src/lifx/products/generator.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/src/lifx/products/quirks.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/src/lifx/products/registry.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/src/lifx/protocol/__init__.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/src/lifx/protocol/generator.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/src/lifx/protocol/models.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/src/lifx/protocol/packets.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/src/lifx/protocol/protocol_types.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/src/lifx/protocol/serializer.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/src/lifx/py.typed +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/src/lifx/theme/__init__.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/src/lifx/theme/canvas.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/src/lifx/theme/generators.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/src/lifx/theme/library.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/src/lifx/theme/theme.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/tests/__init__.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/tests/conftest.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/tests/test_animation/__init__.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/tests/test_animation/conftest.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/tests/test_animation/test_animator.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/tests/test_animation/test_framebuffer.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/tests/test_animation/test_orientation.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/tests/test_api/__init__.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/tests/test_api/test_api_apply_theme.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/tests/test_api/test_api_batch_errors.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/tests/test_api/test_api_batch_operations.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/tests/test_api/test_api_discovery.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/tests/test_api/test_api_organization.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/tests/test_color.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/tests/test_devices/__init__.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/tests/test_devices/conftest.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/tests/test_devices/test_base.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/tests/test_devices/test_ceiling.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/tests/test_devices/test_hev.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/tests/test_devices/test_infrared.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/tests/test_devices/test_light.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/tests/test_devices/test_mac_address.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/tests/test_devices/test_matrix.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/tests/test_devices/test_multizone.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/tests/test_devices/test_state_ceiling.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/tests/test_devices/test_state_hev.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/tests/test_devices/test_state_infrared.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/tests/test_devices/test_state_matrix.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/tests/test_devices/test_state_multizone.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/tests/test_effects/__init__.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/tests/test_effects/test_base.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/tests/test_effects/test_capability_filtering.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/tests/test_effects/test_colorloop.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/tests/test_effects/test_integration.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/tests/test_effects/test_models.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/tests/test_effects/test_pulse.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/tests/test_effects/test_state_manager.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/tests/test_network/__init__.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/tests/test_network/test_concurrent_requests.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/tests/test_network/test_discovery_devices.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/tests/test_network/test_discovery_errors.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/tests/test_network/test_mdns/__init__.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/tests/test_network/test_mdns/conftest.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/tests/test_network/test_mdns/test_discovery.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/tests/test_network/test_mdns/test_dns.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/tests/test_network/test_mdns/test_transport.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/tests/test_network/test_message.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/tests/test_network/test_message_advanced.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/tests/test_network/test_transport.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/tests/test_products/test_product_generator.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/tests/test_products/test_registry.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/tests/test_protocol/test_generated.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/tests/test_protocol/test_header.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/tests/test_protocol/test_protocol_generator.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/tests/test_protocol/test_serializer.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/tests/test_theme/__init__.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/tests/test_theme/conftest.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/tests/test_theme/test_apply_theme.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/tests/test_theme/test_canvas.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/tests/test_theme/test_generators.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/tests/test_theme/test_library.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/tests/test_theme/test_theme.py +0 -0
- {lifx_async-5.1.0 → lifx_async-5.1.1}/tests/test_utils.py +0 -0
|
@@ -32,7 +32,7 @@ jobs:
|
|
|
32
32
|
name: Code Quality
|
|
33
33
|
runs-on: ubuntu-latest
|
|
34
34
|
steps:
|
|
35
|
-
- uses: actions/checkout@
|
|
35
|
+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
|
36
36
|
|
|
37
37
|
- name: Set up Python
|
|
38
38
|
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
|
|
@@ -40,7 +40,7 @@ jobs:
|
|
|
40
40
|
python-version: ${{ env.PYTHON_VERSION }}
|
|
41
41
|
|
|
42
42
|
- name: Install uv
|
|
43
|
-
uses: astral-sh/setup-uv@
|
|
43
|
+
uses: astral-sh/setup-uv@803947b9bd8e9f986429fa0c5a41c367cd732b41 # v7
|
|
44
44
|
with:
|
|
45
45
|
version: ${{ env.UV_VERSION }}
|
|
46
46
|
python-version: ${{ env.PYTHON_VERSION }}
|
|
@@ -68,7 +68,7 @@ jobs:
|
|
|
68
68
|
needs: quality
|
|
69
69
|
runs-on: ubuntu-latest
|
|
70
70
|
steps:
|
|
71
|
-
- uses: actions/checkout@
|
|
71
|
+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
|
72
72
|
|
|
73
73
|
- name: Set up Python
|
|
74
74
|
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
|
|
@@ -76,7 +76,7 @@ jobs:
|
|
|
76
76
|
python-version: ${{ env.PYTHON_VERSION }}
|
|
77
77
|
|
|
78
78
|
- name: Install uv
|
|
79
|
-
uses: astral-sh/setup-uv@
|
|
79
|
+
uses: astral-sh/setup-uv@803947b9bd8e9f986429fa0c5a41c367cd732b41 # v7
|
|
80
80
|
with:
|
|
81
81
|
version: ${{ env.UV_VERSION }}
|
|
82
82
|
python-version: ${{ env.PYTHON_VERSION }}
|
|
@@ -115,7 +115,7 @@ jobs:
|
|
|
115
115
|
os: [ubuntu-latest, macos-latest, windows-latest]
|
|
116
116
|
python-version: ['3.10', '3.11', '3.12', '3.13', '3.14']
|
|
117
117
|
steps:
|
|
118
|
-
- uses: actions/checkout@
|
|
118
|
+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
|
119
119
|
|
|
120
120
|
- name: Set up Python ${{ matrix.python-version }}
|
|
121
121
|
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
|
|
@@ -123,7 +123,7 @@ jobs:
|
|
|
123
123
|
python-version: ${{ matrix.python-version }}
|
|
124
124
|
|
|
125
125
|
- name: Install uv
|
|
126
|
-
uses: astral-sh/setup-uv@
|
|
126
|
+
uses: astral-sh/setup-uv@803947b9bd8e9f986429fa0c5a41c367cd732b41 # v7
|
|
127
127
|
with:
|
|
128
128
|
version: ${{ env.UV_VERSION }}
|
|
129
129
|
python-version: ${{ matrix.python-version }}
|
|
@@ -167,7 +167,7 @@ jobs:
|
|
|
167
167
|
if: github.event_name == 'push' && github.ref_name == 'main'
|
|
168
168
|
runs-on: ubuntu-latest
|
|
169
169
|
steps:
|
|
170
|
-
- uses: actions/checkout@
|
|
170
|
+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
|
171
171
|
|
|
172
172
|
- name: Set up Python
|
|
173
173
|
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
|
|
@@ -175,7 +175,7 @@ jobs:
|
|
|
175
175
|
python-version: ${{ env.PYTHON_VERSION }}
|
|
176
176
|
|
|
177
177
|
- name: Install uv
|
|
178
|
-
uses: astral-sh/setup-uv@
|
|
178
|
+
uses: astral-sh/setup-uv@803947b9bd8e9f986429fa0c5a41c367cd732b41 # v7
|
|
179
179
|
with:
|
|
180
180
|
version: ${{ env.UV_VERSION }}
|
|
181
181
|
python-version: ${{ env.PYTHON_VERSION }}
|
|
@@ -228,7 +228,7 @@ jobs:
|
|
|
228
228
|
|
|
229
229
|
steps:
|
|
230
230
|
- name: Checkout repository on release branch
|
|
231
|
-
uses: actions/checkout@
|
|
231
|
+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
|
232
232
|
with:
|
|
233
233
|
ref: ${{ github.head_ref || github.ref_name }}
|
|
234
234
|
fetch-depth: 0
|
|
@@ -240,7 +240,7 @@ jobs:
|
|
|
240
240
|
python-version: ${{ env.PYTHON_VERSION }}
|
|
241
241
|
|
|
242
242
|
- name: Install uv
|
|
243
|
-
uses: astral-sh/setup-uv@
|
|
243
|
+
uses: astral-sh/setup-uv@803947b9bd8e9f986429fa0c5a41c367cd732b41 # v7
|
|
244
244
|
with:
|
|
245
245
|
version: ${{ env.UV_VERSION }}
|
|
246
246
|
python-version: ${{ env.PYTHON_VERSION }}
|
|
@@ -25,7 +25,7 @@ jobs:
|
|
|
25
25
|
build-docs:
|
|
26
26
|
runs-on: ubuntu-latest
|
|
27
27
|
steps:
|
|
28
|
-
- uses: actions/checkout@
|
|
28
|
+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
|
29
29
|
with:
|
|
30
30
|
fetch-depth: 0 # Fetch all history for git-revision-date-localized
|
|
31
31
|
|
|
@@ -35,7 +35,7 @@ jobs:
|
|
|
35
35
|
python-version: ${{ env.PYTHON_VERSION }}
|
|
36
36
|
|
|
37
37
|
- name: Install uv
|
|
38
|
-
uses: astral-sh/setup-uv@
|
|
38
|
+
uses: astral-sh/setup-uv@803947b9bd8e9f986429fa0c5a41c367cd732b41 # v7
|
|
39
39
|
with:
|
|
40
40
|
version: ${{ env.UV_VERSION }}
|
|
41
41
|
python-version: ${{ env.PYTHON_VERSION }}
|
|
@@ -59,7 +59,7 @@ jobs:
|
|
|
59
59
|
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
|
|
60
60
|
needs: build-docs
|
|
61
61
|
steps:
|
|
62
|
-
- uses: actions/checkout@
|
|
62
|
+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
|
63
63
|
with:
|
|
64
64
|
fetch-depth: 0
|
|
65
65
|
|
|
@@ -69,7 +69,7 @@ jobs:
|
|
|
69
69
|
python-version: ${{ env.PYTHON_VERSION }}
|
|
70
70
|
|
|
71
71
|
- name: Install uv
|
|
72
|
-
uses: astral-sh/setup-uv@
|
|
72
|
+
uses: astral-sh/setup-uv@803947b9bd8e9f986429fa0c5a41c367cd732b41 # v7
|
|
73
73
|
with:
|
|
74
74
|
version: ${{ env.UV_VERSION }}
|
|
75
75
|
python-version: ${{ env.PYTHON_VERSION }}
|
|
@@ -89,7 +89,7 @@ jobs:
|
|
|
89
89
|
validate-links:
|
|
90
90
|
runs-on: ubuntu-latest
|
|
91
91
|
steps:
|
|
92
|
-
- uses: actions/checkout@
|
|
92
|
+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
|
93
93
|
|
|
94
94
|
- name: Set up Python
|
|
95
95
|
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
|
|
@@ -97,7 +97,7 @@ jobs:
|
|
|
97
97
|
python-version: ${{ env.PYTHON_VERSION }}
|
|
98
98
|
|
|
99
99
|
- name: Install uv
|
|
100
|
-
uses: astral-sh/setup-uv@
|
|
100
|
+
uses: astral-sh/setup-uv@803947b9bd8e9f986429fa0c5a41c367cd732b41 # v7
|
|
101
101
|
with:
|
|
102
102
|
version: ${{ env.UV_VERSION }}
|
|
103
103
|
python-version: ${{ env.PYTHON_VERSION }}
|
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
<!-- version list -->
|
|
4
4
|
|
|
5
|
+
## v5.1.1 (2026-02-06)
|
|
6
|
+
|
|
7
|
+
### Performance Improvements
|
|
8
|
+
|
|
9
|
+
- Optimize device initialization and packet sending
|
|
10
|
+
([`865c4e3`](https://github.com/Djelibeybi/lifx-async/commit/865c4e32f696881a4e0ed2932eb014c4136d0554))
|
|
11
|
+
|
|
12
|
+
- Pure-Python optimizations for protocol, network, and animation layers
|
|
13
|
+
([`a9a4bd4`](https://github.com/Djelibeybi/lifx-async/commit/a9a4bd40e35095c079339b336d82f1f5d509560a))
|
|
14
|
+
|
|
15
|
+
|
|
5
16
|
## v5.1.0 (2026-01-24)
|
|
6
17
|
|
|
7
18
|
### Bug Fixes
|
|
@@ -6,14 +6,30 @@ Path) or multizone (Strip, Beam) device and runs an appropriate animation.
|
|
|
6
6
|
|
|
7
7
|
The animation module sends frames via direct UDP for maximum throughput -
|
|
8
8
|
no connection layer overhead, no ACKs, just fire packets as fast as possible.
|
|
9
|
+
|
|
10
|
+
Use --profile to run performance benchmarks:
|
|
11
|
+
--profile alone: runs synthetic micro-benchmarks (no device needed)
|
|
12
|
+
--profile with --serial/--ip: also profiles the animation loop against a device
|
|
9
13
|
"""
|
|
10
14
|
|
|
11
15
|
import argparse
|
|
12
16
|
import asyncio
|
|
13
17
|
import math
|
|
18
|
+
import statistics
|
|
14
19
|
import time
|
|
15
20
|
|
|
16
|
-
from lifx import
|
|
21
|
+
from lifx import (
|
|
22
|
+
Animator,
|
|
23
|
+
Device,
|
|
24
|
+
MatrixLight,
|
|
25
|
+
MultiZoneLight,
|
|
26
|
+
find_by_ip,
|
|
27
|
+
find_by_serial,
|
|
28
|
+
)
|
|
29
|
+
from lifx.animation.packets import MatrixPacketGenerator, MultiZonePacketGenerator
|
|
30
|
+
from lifx.protocol.header import LifxHeader
|
|
31
|
+
from lifx.protocol.packets import Light
|
|
32
|
+
from lifx.protocol.protocol_types import LightHsbk
|
|
17
33
|
|
|
18
34
|
|
|
19
35
|
def print_animator_info(animator: Animator) -> None:
|
|
@@ -25,6 +41,207 @@ def print_animator_info(animator: Animator) -> None:
|
|
|
25
41
|
print("---------------------\n")
|
|
26
42
|
|
|
27
43
|
|
|
44
|
+
def percentile_stats(times_ms: list[float]) -> dict[str, float]:
|
|
45
|
+
"""Compute summary statistics from a list of times in milliseconds."""
|
|
46
|
+
if not times_ms:
|
|
47
|
+
return {
|
|
48
|
+
"mean": 0.0,
|
|
49
|
+
"median": 0.0,
|
|
50
|
+
"p95": 0.0,
|
|
51
|
+
"p99": 0.0,
|
|
52
|
+
"min": 0.0,
|
|
53
|
+
"max": 0.0,
|
|
54
|
+
}
|
|
55
|
+
times_ms.sort()
|
|
56
|
+
n = len(times_ms)
|
|
57
|
+
return {
|
|
58
|
+
"mean": statistics.mean(times_ms),
|
|
59
|
+
"median": statistics.median(times_ms),
|
|
60
|
+
"p95": times_ms[int(n * 0.95)],
|
|
61
|
+
"p99": times_ms[int(n * 0.99)],
|
|
62
|
+
"min": times_ms[0],
|
|
63
|
+
"max": times_ms[-1],
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def print_stats(label: str, stats: dict[str, float]) -> None:
|
|
68
|
+
"""Print a single stats line."""
|
|
69
|
+
print(
|
|
70
|
+
f" {label}:\n"
|
|
71
|
+
f" mean={stats['mean']:.3f}ms median={stats['median']:.3f}ms "
|
|
72
|
+
f"p95={stats['p95']:.3f}ms p99={stats['p99']:.3f}ms "
|
|
73
|
+
f"min={stats['min']:.3f}ms max={stats['max']:.3f}ms"
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
async def run_profile(
|
|
78
|
+
device: MatrixLight | MultiZoneLight,
|
|
79
|
+
iterations: int = 2000,
|
|
80
|
+
warmup: int = 200,
|
|
81
|
+
) -> None:
|
|
82
|
+
"""Profile the animation loop against a real device."""
|
|
83
|
+
is_matrix = isinstance(device, MatrixLight)
|
|
84
|
+
|
|
85
|
+
if is_matrix:
|
|
86
|
+
animator = await Animator.for_matrix(device)
|
|
87
|
+
canvas_width = animator.canvas_width
|
|
88
|
+
canvas_height = animator.canvas_height
|
|
89
|
+
else:
|
|
90
|
+
animator = await Animator.for_multizone(device)
|
|
91
|
+
canvas_width = animator.pixel_count
|
|
92
|
+
canvas_height = 1
|
|
93
|
+
|
|
94
|
+
pixel_count = animator.pixel_count
|
|
95
|
+
|
|
96
|
+
# Pre-compute wave constants for matrix
|
|
97
|
+
wave_angle = math.radians(30)
|
|
98
|
+
cos_wave = math.cos(wave_angle)
|
|
99
|
+
sin_wave = math.sin(wave_angle)
|
|
100
|
+
max_pos = canvas_width * cos_wave + canvas_height * sin_wave
|
|
101
|
+
|
|
102
|
+
gen_times: list[float] = []
|
|
103
|
+
send_times: list[float] = []
|
|
104
|
+
|
|
105
|
+
total_iterations = warmup + iterations
|
|
106
|
+
print(
|
|
107
|
+
f"\n=== Animation Profile ({iterations} iterations, "
|
|
108
|
+
f"{'matrix' if is_matrix else 'multizone'} "
|
|
109
|
+
f"{canvas_width}x{canvas_height}, {pixel_count} pixels) ==="
|
|
110
|
+
)
|
|
111
|
+
print(f" Warmup: {warmup} iterations")
|
|
112
|
+
|
|
113
|
+
try:
|
|
114
|
+
for i in range(total_iterations):
|
|
115
|
+
hue_offset = (i * 1000) % 65536
|
|
116
|
+
|
|
117
|
+
# Time frame generation
|
|
118
|
+
t0 = time.perf_counter()
|
|
119
|
+
if is_matrix:
|
|
120
|
+
frame = []
|
|
121
|
+
for y in range(canvas_height):
|
|
122
|
+
for x in range(canvas_width):
|
|
123
|
+
pos = x * cos_wave + y * sin_wave
|
|
124
|
+
hue = int((pos / max_pos) * 65535 + hue_offset) % 65536
|
|
125
|
+
frame.append((hue, 65535, 65535, 3500))
|
|
126
|
+
else:
|
|
127
|
+
frame = []
|
|
128
|
+
for j in range(pixel_count):
|
|
129
|
+
hue_val = int((j / pixel_count) * 65536)
|
|
130
|
+
hue = (hue_offset + hue_val) % 65536
|
|
131
|
+
frame.append((hue, 65535, 65535, 3500))
|
|
132
|
+
t1 = time.perf_counter()
|
|
133
|
+
|
|
134
|
+
# Time send_frame
|
|
135
|
+
animator.send_frame(frame)
|
|
136
|
+
t2 = time.perf_counter()
|
|
137
|
+
|
|
138
|
+
if i >= warmup:
|
|
139
|
+
gen_times.append((t1 - t0) * 1000)
|
|
140
|
+
send_times.append((t2 - t1) * 1000)
|
|
141
|
+
finally:
|
|
142
|
+
animator.close()
|
|
143
|
+
|
|
144
|
+
total_times = [g + s for g, s in zip(gen_times, send_times)]
|
|
145
|
+
|
|
146
|
+
print()
|
|
147
|
+
print_stats("Frame generation", percentile_stats(gen_times))
|
|
148
|
+
print_stats("send_frame (orient + pack + send)", percentile_stats(send_times))
|
|
149
|
+
print_stats("Total per-frame", percentile_stats(total_times))
|
|
150
|
+
|
|
151
|
+
if total_times:
|
|
152
|
+
mean_total = statistics.mean(total_times)
|
|
153
|
+
if mean_total > 0:
|
|
154
|
+
throughput = 1000.0 / mean_total
|
|
155
|
+
print(f"\n Throughput: {throughput:,.0f} frames/sec")
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _bench(label: str, func: object, n: int) -> None:
|
|
159
|
+
"""Run a tight-loop benchmark and print results."""
|
|
160
|
+
t0 = time.perf_counter()
|
|
161
|
+
for _ in range(n):
|
|
162
|
+
func() # type: ignore[operator]
|
|
163
|
+
elapsed = time.perf_counter() - t0
|
|
164
|
+
rate = n / elapsed
|
|
165
|
+
per_call = elapsed / n * 1000
|
|
166
|
+
print(
|
|
167
|
+
f"{label}:\n"
|
|
168
|
+
f" {n:,} calls in {elapsed:.2f}s "
|
|
169
|
+
f"({rate:,.0f} calls/sec, {per_call:.4f}ms/call)"
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def run_synthetic_benchmarks() -> None:
|
|
174
|
+
"""Run synthetic micro-benchmarks of optimized code paths."""
|
|
175
|
+
print("\n=== Synthetic Benchmarks ===\n")
|
|
176
|
+
|
|
177
|
+
dummy_target = b"\xd0\x73\xd5\x01\x02\x03"
|
|
178
|
+
dummy_source = 12345
|
|
179
|
+
|
|
180
|
+
# --- update_colors: matrix (5 tiles, 320 pixels) ---
|
|
181
|
+
matrix_gen = MatrixPacketGenerator(tile_count=5, tile_width=8, tile_height=8)
|
|
182
|
+
matrix_templates = matrix_gen.create_templates(dummy_source, dummy_target)
|
|
183
|
+
matrix_pixels = matrix_gen.pixel_count()
|
|
184
|
+
matrix_hsbk = [(32768, 65535, 32768, 3500)] * matrix_pixels
|
|
185
|
+
|
|
186
|
+
n = 100_000
|
|
187
|
+
_bench(
|
|
188
|
+
f"update_colors (matrix, 5 tiles, {matrix_pixels}px)",
|
|
189
|
+
lambda: matrix_gen.update_colors(matrix_templates, matrix_hsbk),
|
|
190
|
+
n,
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
# --- update_colors: multizone (82 zones) ---
|
|
194
|
+
mz_gen = MultiZonePacketGenerator(zone_count=82)
|
|
195
|
+
mz_templates = mz_gen.create_templates(dummy_source, dummy_target)
|
|
196
|
+
mz_hsbk = [(32768, 65535, 32768, 3500)] * 82
|
|
197
|
+
|
|
198
|
+
print()
|
|
199
|
+
_bench(
|
|
200
|
+
"update_colors (multizone, 82 zones)",
|
|
201
|
+
lambda: mz_gen.update_colors(mz_templates, mz_hsbk),
|
|
202
|
+
n,
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
# --- LifxHeader.pack() ---
|
|
206
|
+
header = LifxHeader.create(
|
|
207
|
+
pkt_type=102,
|
|
208
|
+
source=dummy_source,
|
|
209
|
+
target=dummy_target,
|
|
210
|
+
payload_size=13,
|
|
211
|
+
)
|
|
212
|
+
n_header = 500_000
|
|
213
|
+
|
|
214
|
+
print()
|
|
215
|
+
_bench("LifxHeader.pack()", header.pack, n_header)
|
|
216
|
+
|
|
217
|
+
# --- LifxHeader.unpack() ---
|
|
218
|
+
packed_header = header.pack()
|
|
219
|
+
|
|
220
|
+
print()
|
|
221
|
+
_bench(
|
|
222
|
+
"LifxHeader.unpack()",
|
|
223
|
+
lambda: LifxHeader.unpack(packed_header),
|
|
224
|
+
n_header,
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
# --- Packet.pack (Light.SetColor) ---
|
|
228
|
+
hsbk = LightHsbk(hue=32768, saturation=65535, brightness=32768, kelvin=3500)
|
|
229
|
+
packet = Light.SetColor(color=hsbk, duration=1000)
|
|
230
|
+
|
|
231
|
+
print()
|
|
232
|
+
_bench("Packet.pack (Light.SetColor)", packet.pack, n)
|
|
233
|
+
|
|
234
|
+
# --- Packet.unpack (Light.SetColor) ---
|
|
235
|
+
packed_packet = packet.pack()
|
|
236
|
+
|
|
237
|
+
print()
|
|
238
|
+
_bench(
|
|
239
|
+
"Packet.unpack (Light.SetColor)",
|
|
240
|
+
lambda: Light.SetColor.unpack(packed_packet),
|
|
241
|
+
n,
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
|
|
28
245
|
async def run_matrix_animation(
|
|
29
246
|
device: MatrixLight,
|
|
30
247
|
duration: float = 10.0,
|
|
@@ -216,27 +433,44 @@ async def run_multizone_animation(
|
|
|
216
433
|
|
|
217
434
|
|
|
218
435
|
async def main(
|
|
219
|
-
serial: str,
|
|
436
|
+
serial: str | None = None,
|
|
220
437
|
ip: str | None = None,
|
|
221
438
|
duration: float = 10.0,
|
|
222
439
|
fps: float = 30.0,
|
|
440
|
+
profile: bool = False,
|
|
441
|
+
iterations: int = 2000,
|
|
442
|
+
warmup: int = 200,
|
|
223
443
|
) -> None:
|
|
224
444
|
"""Find device and run appropriate animation."""
|
|
445
|
+
if profile and not serial and not ip:
|
|
446
|
+
# Synthetic-only mode
|
|
447
|
+
print("=" * 70)
|
|
448
|
+
print("LIFX Animation Profiler (synthetic benchmarks only)")
|
|
449
|
+
print("=" * 70)
|
|
450
|
+
run_synthetic_benchmarks()
|
|
451
|
+
return
|
|
452
|
+
|
|
453
|
+
if not serial and not ip:
|
|
454
|
+
print("Error: --serial or --ip is required (unless using --profile alone)")
|
|
455
|
+
return
|
|
456
|
+
|
|
225
457
|
print("=" * 70)
|
|
226
|
-
print("LIFX Animation Example")
|
|
458
|
+
print("LIFX Animation Example" + (" (profiling)" if profile else ""))
|
|
227
459
|
print("=" * 70)
|
|
228
460
|
|
|
229
461
|
# Find the device
|
|
230
|
-
if ip:
|
|
462
|
+
if serial and ip:
|
|
463
|
+
# Both serial and IP provided - connect directly without discovery
|
|
464
|
+
print(f"\nConnecting directly to {ip} (serial: {serial})")
|
|
465
|
+
device = await Device.connect(ip=ip, serial=serial)
|
|
466
|
+
elif ip:
|
|
231
467
|
print(f"\nSearching for device at IP: {ip}")
|
|
232
468
|
device = await find_by_ip(ip)
|
|
233
469
|
if device is None:
|
|
234
470
|
print(f"No device found at IP '{ip}'")
|
|
235
471
|
return
|
|
236
|
-
# Verify serial matches if both provided
|
|
237
|
-
if device.serial.lower().replace(":", "") != serial.lower().replace(":", ""):
|
|
238
|
-
print(f"Warning: Device serial {device.serial} doesn't match {serial}")
|
|
239
472
|
else:
|
|
473
|
+
assert serial is not None
|
|
240
474
|
print(f"\nSearching for device with serial: {serial}")
|
|
241
475
|
device = await find_by_serial(serial)
|
|
242
476
|
if device is None:
|
|
@@ -294,20 +528,32 @@ async def main(
|
|
|
294
528
|
await device.set_power(True)
|
|
295
529
|
await asyncio.sleep(1)
|
|
296
530
|
|
|
297
|
-
# Run appropriate animation
|
|
531
|
+
# Run appropriate animation or profile
|
|
298
532
|
try:
|
|
299
|
-
if
|
|
300
|
-
|
|
301
|
-
|
|
533
|
+
if profile:
|
|
534
|
+
if is_matrix:
|
|
535
|
+
assert isinstance(device, MatrixLight)
|
|
536
|
+
await run_profile(device, iterations, warmup)
|
|
537
|
+
else:
|
|
538
|
+
assert isinstance(device, MultiZoneLight)
|
|
539
|
+
await run_profile(device, iterations, warmup)
|
|
302
540
|
else:
|
|
303
|
-
|
|
304
|
-
|
|
541
|
+
if is_matrix:
|
|
542
|
+
assert isinstance(device, MatrixLight)
|
|
543
|
+
await run_matrix_animation(device, duration, fps)
|
|
544
|
+
else:
|
|
545
|
+
assert isinstance(device, MultiZoneLight)
|
|
546
|
+
await run_multizone_animation(device, duration, fps)
|
|
305
547
|
finally:
|
|
306
548
|
# Restore power state
|
|
307
549
|
if was_off:
|
|
308
550
|
print("\nTurning device back OFF...")
|
|
309
551
|
await device.set_power(False)
|
|
310
552
|
|
|
553
|
+
# Always run synthetic benchmarks after device profiling
|
|
554
|
+
if profile:
|
|
555
|
+
run_synthetic_benchmarks()
|
|
556
|
+
|
|
311
557
|
print("\n" + "=" * 70)
|
|
312
558
|
|
|
313
559
|
|
|
@@ -326,6 +572,12 @@ Examples:
|
|
|
326
572
|
# Run animation for 30 seconds at 60 FPS
|
|
327
573
|
python 15_animation.py --serial d073d5123456 --duration 30 --fps 60
|
|
328
574
|
|
|
575
|
+
# Run synthetic benchmarks only (no device needed)
|
|
576
|
+
python 15_animation.py --profile
|
|
577
|
+
|
|
578
|
+
# Profile animation loop against a device + synthetic benchmarks
|
|
579
|
+
python 15_animation.py --serial d073d5123456 --ip 192.168.1.100 --profile
|
|
580
|
+
|
|
329
581
|
# Serial number formats (both work):
|
|
330
582
|
python 15_animation.py --serial d073d5123456
|
|
331
583
|
python 15_animation.py --serial d0:73:d5:12:34:56
|
|
@@ -334,7 +586,6 @@ Examples:
|
|
|
334
586
|
parser.add_argument(
|
|
335
587
|
"--serial",
|
|
336
588
|
"-s",
|
|
337
|
-
required=True,
|
|
338
589
|
help="Device serial number (12 hex digits, with or without colons)",
|
|
339
590
|
)
|
|
340
591
|
parser.add_argument(
|
|
@@ -356,9 +607,29 @@ Examples:
|
|
|
356
607
|
default=30.0,
|
|
357
608
|
help="Target frames per second (default: 30)",
|
|
358
609
|
)
|
|
610
|
+
parser.add_argument(
|
|
611
|
+
"--profile",
|
|
612
|
+
action="store_true",
|
|
613
|
+
help="Enable profiling mode (no sleep between frames)",
|
|
614
|
+
)
|
|
615
|
+
parser.add_argument(
|
|
616
|
+
"--iterations",
|
|
617
|
+
type=int,
|
|
618
|
+
default=2000,
|
|
619
|
+
help="Number of profiling iterations (default: 2000)",
|
|
620
|
+
)
|
|
621
|
+
parser.add_argument(
|
|
622
|
+
"--warmup",
|
|
623
|
+
type=int,
|
|
624
|
+
default=200,
|
|
625
|
+
help="Warmup iterations excluded from stats (default: 200)",
|
|
626
|
+
)
|
|
359
627
|
|
|
360
628
|
args = parser.parse_args()
|
|
361
629
|
|
|
630
|
+
if not args.profile and not args.serial:
|
|
631
|
+
parser.error("--serial is required unless using --profile alone")
|
|
632
|
+
|
|
362
633
|
try:
|
|
363
634
|
asyncio.run(
|
|
364
635
|
main(
|
|
@@ -366,6 +637,9 @@ Examples:
|
|
|
366
637
|
args.ip,
|
|
367
638
|
args.duration,
|
|
368
639
|
args.fps,
|
|
640
|
+
args.profile,
|
|
641
|
+
args.iterations,
|
|
642
|
+
args.warmup,
|
|
369
643
|
)
|
|
370
644
|
)
|
|
371
645
|
except KeyboardInterrupt:
|