lifx-emulator 2.2.1__tar.gz → 2.3.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_emulator-2.2.1 → lifx_emulator-2.3.1}/CLAUDE.md +21 -5
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/PKG-INFO +1 -1
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/docs/advanced/device-management-api.md +2 -2
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/docs/advanced/scenario-api.md +2 -2
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/docs/api/device.md +2 -2
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/docs/api/products.md +2 -2
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/docs/api/protocol.md +2 -2
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/docs/architecture/overview.md +1 -1
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/docs/changelog.md +16 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/docs/getting-started/cli.md +4 -4
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/docs/guide/device-types.md +12 -1
- lifx_emulator-2.3.1/docs/guide/framebuffers.md +209 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/docs/guide/products-and-specs.md +10 -10
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/docs/guide/web-interface.md +3 -3
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/docs/index.md +1 -1
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/docs/tutorials/04-advanced-scenarios.md +1 -1
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/pyproject.toml +3 -7
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/src/lifx_emulator/__main__.py +2 -2
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/src/lifx_emulator/api/models.py +2 -2
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/src/lifx_emulator/api/templates/dashboard.html +8 -8
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/src/lifx_emulator/devices/device.py +9 -3
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/src/lifx_emulator/devices/state_serializer.py +27 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/src/lifx_emulator/devices/states.py +31 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/src/lifx_emulator/factories/builder.py +2 -2
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/src/lifx_emulator/factories/factory.py +4 -4
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/src/lifx_emulator/handlers/light_handlers.py +155 -20
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/src/lifx_emulator/handlers/tile_handlers.py +147 -22
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/src/lifx_emulator/products/generator.py +2 -2
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/src/lifx_emulator/products/specs.py +2 -2
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/src/lifx_emulator/products/specs.yml +8 -8
- lifx_emulator-2.3.1/tests/test_backwards_compatibility.py +943 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/tests/test_device.py +1 -1
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/tests/test_integration.py +2 -2
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/tests/test_tile_handlers_extended.py +296 -1
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/uv.lock +1 -1
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/.github/workflows/ci.yml +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/.github/workflows/docs.yml +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/.gitignore +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/.pre-commit-config.yaml +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/LICENSE +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/README.md +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/docs/advanced/index.md +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/docs/advanced/scenarios.md +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/docs/advanced/storage.md +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/docs/api/factories.md +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/docs/api/index.md +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/docs/api/server.md +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/docs/api/storage.md +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/docs/architecture/device-state.md +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/docs/architecture/index.md +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/docs/architecture/packet-flow.md +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/docs/architecture/protocol.md +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/docs/assets/favicon.png +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/docs/faq.md +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/docs/getting-started/index.md +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/docs/getting-started/installation.md +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/docs/getting-started/quickstart.md +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/docs/guide/best-practices.md +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/docs/guide/index.md +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/docs/guide/integration-testing.md +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/docs/guide/testing-scenarios.md +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/docs/reference/glossary.md +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/docs/reference/troubleshooting.md +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/docs/stylesheets/extra.css +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/docs/tutorials/01-first-device.md +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/docs/tutorials/02-basic.md +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/docs/tutorials/03-integration.md +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/docs/tutorials/05-cicd.md +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/docs/tutorials/index.md +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/mkdocs.yml +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/renovate.json +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/src/lifx_emulator/__init__.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/src/lifx_emulator/api/__init__.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/src/lifx_emulator/api/app.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/src/lifx_emulator/api/mappers/__init__.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/src/lifx_emulator/api/mappers/device_mapper.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/src/lifx_emulator/api/routers/__init__.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/src/lifx_emulator/api/routers/devices.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/src/lifx_emulator/api/routers/monitoring.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/src/lifx_emulator/api/routers/scenarios.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/src/lifx_emulator/api/services/__init__.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/src/lifx_emulator/api/services/device_service.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/src/lifx_emulator/constants.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/src/lifx_emulator/devices/__init__.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/src/lifx_emulator/devices/manager.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/src/lifx_emulator/devices/observers.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/src/lifx_emulator/devices/persistence.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/src/lifx_emulator/devices/state_restorer.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/src/lifx_emulator/factories/__init__.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/src/lifx_emulator/factories/default_config.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/src/lifx_emulator/factories/firmware_config.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/src/lifx_emulator/factories/serial_generator.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/src/lifx_emulator/handlers/__init__.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/src/lifx_emulator/handlers/base.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/src/lifx_emulator/handlers/device_handlers.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/src/lifx_emulator/handlers/multizone_handlers.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/src/lifx_emulator/handlers/registry.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/src/lifx_emulator/products/__init__.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/src/lifx_emulator/products/registry.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/src/lifx_emulator/protocol/__init__.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/src/lifx_emulator/protocol/base.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/src/lifx_emulator/protocol/const.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/src/lifx_emulator/protocol/generator.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/src/lifx_emulator/protocol/header.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/src/lifx_emulator/protocol/packets.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/src/lifx_emulator/protocol/protocol_types.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/src/lifx_emulator/protocol/serializer.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/src/lifx_emulator/repositories/__init__.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/src/lifx_emulator/repositories/device_repository.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/src/lifx_emulator/repositories/storage_backend.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/src/lifx_emulator/scenarios/__init__.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/src/lifx_emulator/scenarios/manager.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/src/lifx_emulator/scenarios/models.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/src/lifx_emulator/scenarios/persistence.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/src/lifx_emulator/server.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/tests/conftest.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/tests/test_api.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/tests/test_api_validation.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/tests/test_async_storage.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/tests/test_cli.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/tests/test_cli_validation.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/tests/test_device_edge_cases.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/tests/test_device_handlers_extended.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/tests/test_device_manager.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/tests/test_handler_registry.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/tests/test_light_handlers_extended.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/tests/test_multizone_handlers_extended.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/tests/test_observers.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/tests/test_products_generator.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/tests/test_products_specs.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/tests/test_protocol_generator.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/tests/test_protocol_types_coverage.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/tests/test_repositories.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/tests/test_scenario_manager.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/tests/test_scenario_persistence.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/tests/test_serializer.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/tests/test_server.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.1}/tests/test_state_restorer.py +0 -0
|
@@ -128,8 +128,8 @@ lifx-emulator --help
|
|
|
128
128
|
- `--multizone-extended`: Enable extended multizone support (default: True, use --no-multizone-extended to disable)
|
|
129
129
|
- `--tile`: Number of tile devices
|
|
130
130
|
- `--tile-count`: Tiles per device (uses product default if not specified)
|
|
131
|
-
- `--tile-width`: Width of each tile in
|
|
132
|
-
- `--tile-height`: Height of each tile in
|
|
131
|
+
- `--tile-width`: Width of each tile in zones (uses product default if not specified)
|
|
132
|
+
- `--tile-height`: Height of each tile in zones (uses product default if not specified)
|
|
133
133
|
- `--serial-prefix`: serial prefix (6 hex chars, default: d073d5)
|
|
134
134
|
- `--serial-start`: Starting serial suffix (default: 1)
|
|
135
135
|
- `--api`: Enable HTTP API server for monitoring and management (default: False)
|
|
@@ -140,9 +140,9 @@ lifx-emulator --help
|
|
|
140
140
|
**Product Defaults:**
|
|
141
141
|
Device parameters like `--multizone-zones` and `--tile-count` automatically use product-specific defaults from the specs system when not specified:
|
|
142
142
|
- LIFX Beam: Extended multizone support enabled and 80 zones by default
|
|
143
|
-
- LIFX Tile: 5 tiles of 8x8
|
|
144
|
-
- LIFX Candle: 1 tile of 5x6
|
|
145
|
-
- LIFX Ceiling: 1 tile of 8x8
|
|
143
|
+
- LIFX Tile: 5 tiles of 8x8 zones by default
|
|
144
|
+
- LIFX Candle: 1 tile of 5x6 zones by default
|
|
145
|
+
- LIFX Ceiling: 1 tile of 8x8 zones by default
|
|
146
146
|
- These defaults can be overridden with command-line parameters
|
|
147
147
|
|
|
148
148
|
**Firmware Version:**
|
|
@@ -523,6 +523,9 @@ delay = manager.get_response_delay(502, merged) # 1.0s
|
|
|
523
523
|
- Dataclass holding all device state (color, power, zones, tiles, firmware version, etc.)
|
|
524
524
|
- Capability flags: `has_color`, `has_infrared`, `has_multizone`, `has_matrix`, `has_hev`
|
|
525
525
|
- Initialized differently per device type via factory functions
|
|
526
|
+
- **TileFramebuffers**: Internal dataclass for storing non-visible framebuffers (1-7) per tile
|
|
527
|
+
- Provides `get_framebuffer(fb_index, width, height)` for lazy initialization
|
|
528
|
+
- Framebuffer 0 remains in protocol-defined `tile_devices[i]["colors"]`
|
|
526
529
|
|
|
527
530
|
### Protocol Layer
|
|
528
531
|
|
|
@@ -748,6 +751,19 @@ product.supports_extended_multizone(131148) # False (below requirement)
|
|
|
748
751
|
- Tiles with more than 64 zones (16×8) require multiple Get64 requests with different y coordinates
|
|
749
752
|
- Tile state stored in `DeviceState.tile_devices` list, each with `colors` array
|
|
750
753
|
|
|
754
|
+
#### Framebuffer Support
|
|
755
|
+
Matrix devices support 8 framebuffers (0-7) for advanced rendering:
|
|
756
|
+
- **Framebuffer 0**: The visible buffer, stored in `tile_devices[i]["colors"]`
|
|
757
|
+
- **Framebuffers 1-7**: Non-visible buffers, stored in `MatrixState.tile_framebuffers`
|
|
758
|
+
- **Set64**: Respects `rect.fb_index` to update the specified framebuffer
|
|
759
|
+
- **Get64**: Always returns framebuffer 0 (visible buffer) regardless of request `fb_index`
|
|
760
|
+
- **CopyFrameBuffer**: Copies rectangular zones from one framebuffer to another
|
|
761
|
+
- Can copy from any FB (0-7) to any other FB (0-7)
|
|
762
|
+
- Supports source/destination offsets and partial rectangles
|
|
763
|
+
- Use to make non-visible buffers visible by copying to FB0
|
|
764
|
+
- Framebuffers are lazily initialized on first access
|
|
765
|
+
- Non-visible framebuffers are persisted with device state
|
|
766
|
+
|
|
751
767
|
### Testing Scenarios
|
|
752
768
|
Configure via ScenarioConfig in HierarchicalScenarioManager:
|
|
753
769
|
- `drop_packets`: Dict mapping packet type to drop rate (0.0-1.0, where 1.0 = always drop)
|
|
@@ -266,8 +266,8 @@ Creates a new emulated device by product ID. The device will be added to the emu
|
|
|
266
266
|
- `serial` (optional): Device serial (auto-generated if not provided)
|
|
267
267
|
- `zone_count` (optional): Number of zones for multizone devices
|
|
268
268
|
- `tile_count` (optional): Number of tiles for matrix devices
|
|
269
|
-
- `tile_width` (optional): Width of each tile in
|
|
270
|
-
- `tile_height` (optional): Height of each tile in
|
|
269
|
+
- `tile_width` (optional): Width of each tile in zones
|
|
270
|
+
- `tile_height` (optional): Height of each tile in zones
|
|
271
271
|
- `firmware_major` (optional): Firmware major version
|
|
272
272
|
- `firmware_minor` (optional): Firmware minor version
|
|
273
273
|
|
|
@@ -788,8 +788,8 @@ jobs:
|
|
|
788
788
|
| 506 | StateMultiZone | Response with zone colors |
|
|
789
789
|
| 512 | ExtendedStateMultiZone | Response with extended zones |
|
|
790
790
|
| 701 | GetDeviceChain | Get tile chain info |
|
|
791
|
-
| 707 | Get64 | Get tile
|
|
792
|
-
| 715 | Set64 | Set tile
|
|
791
|
+
| 707 | Get64 | Get tile zone data |
|
|
792
|
+
| 715 | Set64 | Set tile zone data |
|
|
793
793
|
|
|
794
794
|
## Tips and Best Practices
|
|
795
795
|
|
|
@@ -90,8 +90,8 @@ Dataclass holding all stateful information for an emulated LIFX device.
|
|
|
90
90
|
|
|
91
91
|
- **`tile_count`** (`int` = `0`) - Number of tiles in chain
|
|
92
92
|
- **`tile_devices`** (`list[dict]` = `[]`) - Per-tile state (position, colors)
|
|
93
|
-
- **`tile_width`** (`int` = `8`) - Width of each tile in
|
|
94
|
-
- **`tile_height`** (`int` = `8`) - Height of each tile in
|
|
93
|
+
- **`tile_width`** (`int` = `8`) - Width of each tile in zones
|
|
94
|
+
- **`tile_height`** (`int` = `8`) - Height of each tile in zones
|
|
95
95
|
|
|
96
96
|
#### Effects (Waveforms & Animations)
|
|
97
97
|
|
|
@@ -239,8 +239,8 @@ Get detailed specifications for a product.
|
|
|
239
239
|
- `zone_count`: Number of zones (multizone devices)
|
|
240
240
|
- `extended_multizone`: Extended multizone support flag
|
|
241
241
|
- `tile_count`: Default number of tiles (matrix devices)
|
|
242
|
-
- `tile_width`: Tile width in
|
|
243
|
-
- `tile_height`: Tile height in
|
|
242
|
+
- `tile_width`: Tile width in zones (matrix devices)
|
|
243
|
+
- `tile_height`: Tile height in zones (matrix devices)
|
|
244
244
|
|
|
245
245
|
**Example:**
|
|
246
246
|
```python
|
|
@@ -322,8 +322,8 @@ class TileStateDevice:
|
|
|
322
322
|
accel_meas_z: int
|
|
323
323
|
user_x: float # User-configured X position
|
|
324
324
|
user_y: float # User-configured Y position
|
|
325
|
-
width: int # Tile width in
|
|
326
|
-
height: int # Tile height in
|
|
325
|
+
width: int # Tile width in zones (e.g., 8)
|
|
326
|
+
height: int # Tile height in zones (e.g., 8)
|
|
327
327
|
device_version_vendor: int
|
|
328
328
|
device_version_product: int
|
|
329
329
|
device_version_version: int
|
|
@@ -253,7 +253,7 @@ Devices advertise capabilities through boolean flags:
|
|
|
253
253
|
| `has_infrared` | IR brightness | LIFX A19 Night Vision |
|
|
254
254
|
| `has_multizone` | Linear zones | LIFX Z, LIFX Beam |
|
|
255
255
|
| `has_extended_multizone` | >16 zones | LIFX Beam |
|
|
256
|
-
| `has_matrix` | 2D
|
|
256
|
+
| `has_matrix` | 2D zone grid | LIFX Tile, LIFX Candle |
|
|
257
257
|
| `has_hev` | HEV cleaning | LIFX Clean |
|
|
258
258
|
|
|
259
259
|
## Packet Types
|
|
@@ -2,6 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
<!-- version list -->
|
|
4
4
|
|
|
5
|
+
## v2.3.1 (2025-11-18)
|
|
6
|
+
|
|
7
|
+
### Bug Fixes
|
|
8
|
+
|
|
9
|
+
- **tile**: Implement backwards compatibility on tile and multizone devices
|
|
10
|
+
([`be473f7`](https://github.com/Djelibeybi/lifx-emulator/commit/be473f74f9a49be7f07082d5bfba16f5d74f46f6))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
## v2.3.0 (2025-11-18)
|
|
14
|
+
|
|
15
|
+
### Features
|
|
16
|
+
|
|
17
|
+
- **tile**: Implement multi-framebuffer support for matrix devices
|
|
18
|
+
([`16a69ca`](https://github.com/Djelibeybi/lifx-emulator/commit/16a69cacbdf0472d8f7116eb0acfda9808431c5c))
|
|
19
|
+
|
|
20
|
+
|
|
5
21
|
## v2.2.1 (2025-11-18)
|
|
6
22
|
|
|
7
23
|
### Bug Fixes
|
|
@@ -197,16 +197,16 @@ Number of tiles per tile device. If not specified, uses product default (5 for L
|
|
|
197
197
|
- **Default:** `None` (uses product defaults)
|
|
198
198
|
- **Example:** `--tile-count 10`
|
|
199
199
|
|
|
200
|
-
### `--tile-width <
|
|
200
|
+
### `--tile-width <zones>`
|
|
201
201
|
|
|
202
|
-
Width of each tile in
|
|
202
|
+
Width of each tile in zones. If not specified, uses product default (typically 8).
|
|
203
203
|
|
|
204
204
|
- **Default:** `None` (uses product defaults)
|
|
205
205
|
- **Example:** `--tile-width 16`
|
|
206
206
|
|
|
207
|
-
### `--tile-height <
|
|
207
|
+
### `--tile-height <zones>`
|
|
208
208
|
|
|
209
|
-
Height of each tile in
|
|
209
|
+
Height of each tile in zones. If not specified, uses product default (typically 8).
|
|
210
210
|
|
|
211
211
|
- **Default:** `None` (uses product defaults)
|
|
212
212
|
- **Example:** `--tile-height 8`
|
|
@@ -297,7 +297,7 @@ print(f"Tile height: {device.state.tile_height}") # 8
|
|
|
297
297
|
|
|
298
298
|
# Access tile devices
|
|
299
299
|
for i, tile in enumerate(device.state.tile_devices):
|
|
300
|
-
print(f"Tile {i}: {tile.width}x{tile.height}
|
|
300
|
+
print(f"Tile {i}: {tile.width}x{tile.height} zones")
|
|
301
301
|
```
|
|
302
302
|
|
|
303
303
|
### Matrix Packet Types
|
|
@@ -329,6 +329,17 @@ single tile.
|
|
|
329
329
|
# split either by row or column.
|
|
330
330
|
```
|
|
331
331
|
|
|
332
|
+
### Framebuffers (v2.3+)
|
|
333
|
+
|
|
334
|
+
Matrix devices support **8 framebuffers (0-7)** to enable atomic updates of tiles with more than 64 zones:
|
|
335
|
+
|
|
336
|
+
- **Framebuffer 0**: Visible buffer (displayed on device)
|
|
337
|
+
- **Framebuffers 1-7**: Non-visible buffers for preparing content off-screen
|
|
338
|
+
|
|
339
|
+
For large tiles (>64 zones), prepare all zones in a non-visible framebuffer, then use `CopyFrameBuffer` to atomically display them without flicker.
|
|
340
|
+
|
|
341
|
+
See [Framebuffer Guide](framebuffers.md) for complete documentation and examples.
|
|
342
|
+
|
|
332
343
|
|
|
333
344
|
## Using Generic create_device()
|
|
334
345
|
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
# Matrix Framebuffer Support
|
|
2
|
+
|
|
3
|
+
Matrix devices with more than 64 zones require multiple Set64 packets to update all zones. Framebuffers enable atomic updates by allowing you to prepare all zones off-screen before displaying them.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
Matrix devices support **8 framebuffers (0-7)**:
|
|
8
|
+
|
|
9
|
+
- **Framebuffer 0**: The visible buffer displayed on the device
|
|
10
|
+
- **Framebuffers 1-7**: Non-visible buffers for preparing content
|
|
11
|
+
|
|
12
|
+
## Why Framebuffers Matter
|
|
13
|
+
|
|
14
|
+
For large tiles (>64 zones), such as the LIFX Ceiling 13"x26" with 128 zones (16×8):
|
|
15
|
+
|
|
16
|
+
**Without framebuffers:**
|
|
17
|
+
```
|
|
18
|
+
Set64(fb=0, zones 0-63) → Visible immediately (partial update)
|
|
19
|
+
Set64(fb=0, zones 64-127) → Visible immediately (flicker as zones update)
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
**With framebuffers:**
|
|
23
|
+
```
|
|
24
|
+
Set64(fb=1, zones 0-63) → Prepared off-screen
|
|
25
|
+
Set64(fb=1, zones 64-127) → Prepared off-screen
|
|
26
|
+
CopyFrameBuffer(fb=1 → fb=0) → All 128 zones appear atomically
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Framebuffer Operations
|
|
30
|
+
|
|
31
|
+
### Set64 - Update Zones
|
|
32
|
+
|
|
33
|
+
The `rect.fb_index` field specifies which framebuffer to update:
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
from lifx_emulator.protocol.packets import Tile
|
|
37
|
+
from lifx_emulator.protocol.protocol_types import TileBufferRect, LightHsbk
|
|
38
|
+
|
|
39
|
+
# Update visible framebuffer (immediate display)
|
|
40
|
+
rect = TileBufferRect(fb_index=0, x=0, y=0, width=8)
|
|
41
|
+
packet = Tile.Set64(
|
|
42
|
+
tile_index=0,
|
|
43
|
+
rect=rect,
|
|
44
|
+
duration=0,
|
|
45
|
+
colors=[...64 colors...]
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
# Update non-visible framebuffer 1 (off-screen)
|
|
49
|
+
rect = TileBufferRect(fb_index=1, x=0, y=0, width=8)
|
|
50
|
+
packet = Tile.Set64(
|
|
51
|
+
tile_index=0,
|
|
52
|
+
rect=rect,
|
|
53
|
+
duration=0,
|
|
54
|
+
colors=[...64 colors...]
|
|
55
|
+
)
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Get64 - Read Zones
|
|
59
|
+
|
|
60
|
+
Get64 **always returns framebuffer 0** (the visible buffer), regardless of the `fb_index` in the request:
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
# Request can specify any fb_index
|
|
64
|
+
rect = TileBufferRect(fb_index=1, x=0, y=0, width=8)
|
|
65
|
+
packet = Tile.Get64(tile_index=0, rect=rect)
|
|
66
|
+
|
|
67
|
+
# Response will contain framebuffer 0 content
|
|
68
|
+
# Response rect.fb_index is always 0
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### CopyFrameBuffer - Atomic Display
|
|
72
|
+
|
|
73
|
+
Copy zones between framebuffers to make prepared content visible:
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
# Copy entire framebuffer 1 to framebuffer 0 (make visible)
|
|
77
|
+
packet = Tile.CopyFrameBuffer(
|
|
78
|
+
tile_index=0,
|
|
79
|
+
src_fb_index=1,
|
|
80
|
+
dst_fb_index=0,
|
|
81
|
+
src_x=0,
|
|
82
|
+
src_y=0,
|
|
83
|
+
dst_x=0,
|
|
84
|
+
dst_y=0,
|
|
85
|
+
width=16,
|
|
86
|
+
height=8,
|
|
87
|
+
duration=0
|
|
88
|
+
)
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Complete Example: Updating a 16×8 Tile
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
from lifx_emulator import create_tile_device
|
|
95
|
+
from lifx_emulator.protocol.packets import Tile
|
|
96
|
+
from lifx_emulator.protocol.protocol_types import TileBufferRect, LightHsbk
|
|
97
|
+
|
|
98
|
+
# Create 16×8 tile (128 zones)
|
|
99
|
+
device = create_tile_device(
|
|
100
|
+
serial="d073dc000001",
|
|
101
|
+
tile_count=1,
|
|
102
|
+
tile_width=16,
|
|
103
|
+
tile_height=8
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
# Prepare colors for all 128 zones
|
|
107
|
+
red = LightHsbk(hue=0, saturation=65535, brightness=65535, kelvin=3500)
|
|
108
|
+
green = LightHsbk(hue=21845, saturation=65535, brightness=65535, kelvin=3500)
|
|
109
|
+
|
|
110
|
+
# Step 1: Update first 64 zones in framebuffer 1 (rows 0-3)
|
|
111
|
+
rect1 = TileBufferRect(fb_index=1, x=0, y=0, width=16)
|
|
112
|
+
set1 = Tile.Set64(
|
|
113
|
+
tile_index=0,
|
|
114
|
+
rect=rect1,
|
|
115
|
+
duration=0,
|
|
116
|
+
colors=[red] * 64
|
|
117
|
+
)
|
|
118
|
+
device.process_packet(header, set1)
|
|
119
|
+
|
|
120
|
+
# Step 2: Update next 64 zones in framebuffer 1 (rows 4-7)
|
|
121
|
+
rect2 = TileBufferRect(fb_index=1, x=0, y=4, width=16)
|
|
122
|
+
set2 = Tile.Set64(
|
|
123
|
+
tile_index=0,
|
|
124
|
+
rect=rect2,
|
|
125
|
+
duration=0,
|
|
126
|
+
colors=[green] * 64
|
|
127
|
+
)
|
|
128
|
+
device.process_packet(header, set2)
|
|
129
|
+
|
|
130
|
+
# Step 3: Atomically display all 128 zones
|
|
131
|
+
copy = Tile.CopyFrameBuffer(
|
|
132
|
+
tile_index=0,
|
|
133
|
+
src_fb_index=1,
|
|
134
|
+
dst_fb_index=0,
|
|
135
|
+
src_x=0,
|
|
136
|
+
src_y=0,
|
|
137
|
+
dst_x=0,
|
|
138
|
+
dst_y=0,
|
|
139
|
+
width=16,
|
|
140
|
+
height=8,
|
|
141
|
+
duration=0
|
|
142
|
+
)
|
|
143
|
+
device.process_packet(header, copy)
|
|
144
|
+
|
|
145
|
+
# All 128 zones now visible without flicker
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Implementation Details
|
|
149
|
+
|
|
150
|
+
### Storage
|
|
151
|
+
|
|
152
|
+
- **Framebuffer 0**: Stored in `tile_devices[i]["colors"]` (protocol-defined)
|
|
153
|
+
- **Framebuffers 1-7**: Stored in `MatrixState.tile_framebuffers` (internal)
|
|
154
|
+
|
|
155
|
+
### Lazy Initialization
|
|
156
|
+
|
|
157
|
+
Non-visible framebuffers are created on first access:
|
|
158
|
+
|
|
159
|
+
```python
|
|
160
|
+
# First Set64 to framebuffer 2 creates it automatically
|
|
161
|
+
# Initialized with black (hue=0, saturation=0, brightness=0)
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### Persistence
|
|
165
|
+
|
|
166
|
+
Non-visible framebuffers are saved with device state when persistence is enabled:
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
lifx-emulator --persistent --tile 1
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## Best Practices
|
|
173
|
+
|
|
174
|
+
### For Tiles ≤64 Zones
|
|
175
|
+
Update framebuffer 0 directly (no need for off-screen preparation):
|
|
176
|
+
|
|
177
|
+
```python
|
|
178
|
+
rect = TileBufferRect(fb_index=0, x=0, y=0, width=8)
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### For Tiles >64 Zones
|
|
182
|
+
Always use a non-visible framebuffer:
|
|
183
|
+
|
|
184
|
+
1. Prepare all zones in framebuffer 1-7
|
|
185
|
+
2. Use CopyFrameBuffer to make visible
|
|
186
|
+
3. Prevents flicker during multi-packet updates
|
|
187
|
+
|
|
188
|
+
### Partial Updates
|
|
189
|
+
Use CopyFrameBuffer with specific rectangles:
|
|
190
|
+
|
|
191
|
+
```python
|
|
192
|
+
# Copy only top-left 4×4 area
|
|
193
|
+
copy = Tile.CopyFrameBuffer(
|
|
194
|
+
src_fb_index=1,
|
|
195
|
+
dst_fb_index=0,
|
|
196
|
+
src_x=0,
|
|
197
|
+
src_y=0,
|
|
198
|
+
dst_x=0,
|
|
199
|
+
dst_y=0,
|
|
200
|
+
width=4,
|
|
201
|
+
height=4,
|
|
202
|
+
duration=0
|
|
203
|
+
)
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
## Related Documentation
|
|
207
|
+
|
|
208
|
+
- [Device Types](device-types.md#matrix-devices) - Matrix device capabilities
|
|
209
|
+
- [Protocol](../architecture/protocol.md) - LIFX LAN protocol details
|
|
@@ -31,7 +31,7 @@ Contains product-specific details not available in the upstream catalog:
|
|
|
31
31
|
- Min/max zone counts
|
|
32
32
|
- Default tile counts for matrix devices
|
|
33
33
|
- Min/max tile counts
|
|
34
|
-
- Tile dimensions (width x height in
|
|
34
|
+
- Tile dimensions (width x height in zones)
|
|
35
35
|
- Product-specific notes
|
|
36
36
|
|
|
37
37
|
### `specs.py`
|
|
@@ -81,8 +81,8 @@ products:
|
|
|
81
81
|
default_tile_count: <number> # Typical number of tiles in chain
|
|
82
82
|
min_tile_count: <number> # Minimum tiles supported
|
|
83
83
|
max_tile_count: <number> # Maximum tiles supported
|
|
84
|
-
tile_width: <
|
|
85
|
-
tile_height: <
|
|
84
|
+
tile_width: <zones> # Width of each tile
|
|
85
|
+
tile_height: <zones> # Height of each tile
|
|
86
86
|
notes: "<description>"
|
|
87
87
|
```
|
|
88
88
|
|
|
@@ -95,7 +95,7 @@ products:
|
|
|
95
95
|
max_tile_count: 5
|
|
96
96
|
tile_width: 8
|
|
97
97
|
tile_height: 8
|
|
98
|
-
notes: "LIFX Tile, 8x8
|
|
98
|
+
notes: "LIFX Tile, 8x8 zone matrix, chainable up to 5"
|
|
99
99
|
```
|
|
100
100
|
|
|
101
101
|
**Example - LIFX Candle:**
|
|
@@ -107,7 +107,7 @@ products:
|
|
|
107
107
|
max_tile_count: 1
|
|
108
108
|
tile_width: 5
|
|
109
109
|
tile_height: 6
|
|
110
|
-
notes: "LIFX Candle, 5x6
|
|
110
|
+
notes: "LIFX Candle, 5x6 zone matrix, single unit"
|
|
111
111
|
```
|
|
112
112
|
|
|
113
113
|
**Example - LIFX Ceiling:**
|
|
@@ -119,7 +119,7 @@ products:
|
|
|
119
119
|
max_tile_count: 1
|
|
120
120
|
tile_width: 22
|
|
121
121
|
tile_height: 22
|
|
122
|
-
notes: "LIFX Ceiling, 22x22
|
|
122
|
+
notes: "LIFX Ceiling, 22x22 zone matrix"
|
|
123
123
|
```
|
|
124
124
|
|
|
125
125
|
## How Specifications Are Used
|
|
@@ -149,16 +149,16 @@ When creating a matrix device:
|
|
|
149
149
|
2. **Tile count**: From `specs.yml` if not specified by user
|
|
150
150
|
|
|
151
151
|
```python
|
|
152
|
-
# Uses specification: 5 tiles of 8x8
|
|
152
|
+
# Uses specification: 5 tiles of 8x8 zones
|
|
153
153
|
device = create_device(55)
|
|
154
154
|
|
|
155
155
|
# Custom tile count, specification dimensions
|
|
156
|
-
device = create_device(55, tile_count=3) # 3 tiles of 8x8
|
|
156
|
+
device = create_device(55, tile_count=3) # 3 tiles of 8x8 zones
|
|
157
157
|
|
|
158
|
-
# Candle: 1 tile of 5x5
|
|
158
|
+
# Candle: 1 tile of 5x5 zones (from specification)
|
|
159
159
|
device = create_device(57)
|
|
160
160
|
|
|
161
|
-
# Ceiling: 1 tile of 22x22
|
|
161
|
+
# Ceiling: 1 tile of 22x22 zones (from specification)
|
|
162
162
|
device = create_device(176)
|
|
163
163
|
```
|
|
164
164
|
|
|
@@ -142,7 +142,7 @@ For matrix/tile devices:
|
|
|
142
142
|
```
|
|
143
143
|
▸ Show tiles (5) (click to expand)
|
|
144
144
|
┌──────────┐
|
|
145
|
-
│ T1 │ (8×8
|
|
145
|
+
│ T1 │ (8×8 zone grid)
|
|
146
146
|
│ ████████ │
|
|
147
147
|
│ ████████ │
|
|
148
148
|
│ ████████ │
|
|
@@ -249,8 +249,8 @@ For matrix devices (tiles, candles, ceiling):
|
|
|
249
249
|
|
|
250
250
|
1. Locate device in Devices section
|
|
251
251
|
2. Click "▸ Show tiles" to expand
|
|
252
|
-
3. Grid display shows
|
|
253
|
-
4. Each small square is one
|
|
252
|
+
3. Grid display shows zone colors
|
|
253
|
+
4. Each small square is one zone
|
|
254
254
|
5. Tiles labeled T1, T2, etc.
|
|
255
255
|
6. Click again to collapse
|
|
256
256
|
|
|
@@ -71,7 +71,7 @@ LIFX Emulator is a Python library and CLI tool that creates virtual LIFX devices
|
|
|
71
71
|
| Infrared | LIFX A19 Night Vision | IR brightness control |
|
|
72
72
|
| HEV | LIFX Clean | HEV cleaning cycle |
|
|
73
73
|
| Multizone | LIFX Z, LIFX Beam | Linear zones (up to 82) |
|
|
74
|
-
| Matrix | LIFX Tile, LIFX Candle | 2D
|
|
74
|
+
| Matrix | LIFX Tile, LIFX Candle | 2D zone arrays |
|
|
75
75
|
|
|
76
76
|
## Use Cases
|
|
77
77
|
|
|
@@ -84,7 +84,7 @@ async def main():
|
|
|
84
84
|
# Create a LIFX Tile with 5 tiles in the chain
|
|
85
85
|
device = create_tile_device("d073d9000001", tile_count=5)
|
|
86
86
|
|
|
87
|
-
# Each tile is 8x8
|
|
87
|
+
# Each tile is 8x8 zones (64 zones)
|
|
88
88
|
print(f"Tile device configuration:")
|
|
89
89
|
print(f" Tiles: {len(device.state.tile_devices)}")
|
|
90
90
|
for i, tile in enumerate(device.state.tile_devices):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "lifx-emulator"
|
|
3
|
-
version = "2.
|
|
3
|
+
version = "2.3.1"
|
|
4
4
|
description = "LIFX Emulator for testing LIFX LAN protocol libraries"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.11"
|
|
@@ -73,7 +73,7 @@ docstring-code-format = true
|
|
|
73
73
|
docstring-code-line-length = "dynamic"
|
|
74
74
|
|
|
75
75
|
[tool.ruff.lint]
|
|
76
|
-
select = ["E", "F", "I", "N", "W", "UP"
|
|
76
|
+
select = ["E", "F", "I", "N", "W", "UP"]
|
|
77
77
|
ignore = []
|
|
78
78
|
|
|
79
79
|
# Complexity and code quality limits
|
|
@@ -86,12 +86,8 @@ max-branches = 12
|
|
|
86
86
|
max-statements = 50
|
|
87
87
|
|
|
88
88
|
[tool.ruff.lint.per-file-ignores]
|
|
89
|
-
"src/lifx_emulator/
|
|
90
|
-
"src/lifx_emulator/devices/state_restorer.py" = ["C901"]
|
|
91
|
-
"src/lifx_emulator/protocol/generator.py" = ["E501", "C901"]
|
|
89
|
+
"src/lifx_emulator/protocol/generator.py" = ["E501"]
|
|
92
90
|
"src/lifx_emulator/protocol/packets.py" = ["E501"]
|
|
93
|
-
"src/lifx_emulator/products/registry.py" = ["C901"]
|
|
94
|
-
"src/lifx_emulator/scenarios/persistence.py" = ["C901"]
|
|
95
91
|
|
|
96
92
|
|
|
97
93
|
[tool.pyright]
|
|
@@ -286,9 +286,9 @@ async def run(
|
|
|
286
286
|
tile: Number of tile/matrix chain devices.
|
|
287
287
|
tile_count: Number of tiles per device. Uses product defaults if not
|
|
288
288
|
specified (5 for Tile, 1 for Candle/Ceiling).
|
|
289
|
-
tile_width: Width of each tile in
|
|
289
|
+
tile_width: Width of each tile in zones. Uses product defaults if not
|
|
290
290
|
specified (8 for most devices).
|
|
291
|
-
tile_height: Height of each tile in
|
|
291
|
+
tile_height: Height of each tile in zones. Uses product defaults if
|
|
292
292
|
not specified (8 for most devices).
|
|
293
293
|
serial_prefix: Serial number prefix as 6 hex characters.
|
|
294
294
|
serial_start: Starting serial suffix for auto-incrementing device serials.
|
|
@@ -25,10 +25,10 @@ class DeviceCreateRequest(BaseModel):
|
|
|
25
25
|
None, description="Number of tiles for matrix devices", ge=0, le=100
|
|
26
26
|
)
|
|
27
27
|
tile_width: int | None = Field(
|
|
28
|
-
None, description="Width of each tile in
|
|
28
|
+
None, description="Width of each tile in zones", ge=1, le=256
|
|
29
29
|
)
|
|
30
30
|
tile_height: int | None = Field(
|
|
31
|
-
None, description="Height of each tile in
|
|
31
|
+
None, description="Height of each tile in zones", ge=1, le=256
|
|
32
32
|
)
|
|
33
33
|
firmware_major: int | None = Field(
|
|
34
34
|
None, description="Firmware major version", ge=0, le=255
|
|
@@ -163,7 +163,7 @@
|
|
|
163
163
|
gap: 2px;
|
|
164
164
|
margin-top: 4px;
|
|
165
165
|
}
|
|
166
|
-
.tile-
|
|
166
|
+
.tile-zone {
|
|
167
167
|
width: 8px;
|
|
168
168
|
height: 8px;
|
|
169
169
|
border-radius: 1px;
|
|
@@ -644,7 +644,7 @@
|
|
|
644
644
|
`;
|
|
645
645
|
} else if (dev.has_matrix && dev.tile_devices &&
|
|
646
646
|
dev.tile_devices.length > 0) {
|
|
647
|
-
// Render actual tile
|
|
647
|
+
// Render actual tile zones
|
|
648
648
|
const tilesHtml = dev.tile_devices.map((tile, tileIndex) => {
|
|
649
649
|
if (!tile.colors || tile.colors.length === 0) {
|
|
650
650
|
return '<div style="color: #666;">No color data</div>';
|
|
@@ -652,14 +652,14 @@
|
|
|
652
652
|
|
|
653
653
|
const width = tile.width || 8;
|
|
654
654
|
const height = tile.height || 8;
|
|
655
|
-
const
|
|
655
|
+
const totalzones = width * height;
|
|
656
656
|
|
|
657
|
-
// Create grid of
|
|
658
|
-
const slicedColors = tile.colors.slice(0,
|
|
659
|
-
const
|
|
657
|
+
// Create grid of zones
|
|
658
|
+
const slicedColors = tile.colors.slice(0, totalzones);
|
|
659
|
+
const zonesHtml = slicedColors.map(color => {
|
|
660
660
|
const rgb = hsbkToRgb(color);
|
|
661
661
|
const bgStyle = `background: ${rgb};`;
|
|
662
|
-
return `<div class="tile-
|
|
662
|
+
return `<div class="tile-zone" style="${bgStyle}"></div>`;
|
|
663
663
|
}).join('');
|
|
664
664
|
|
|
665
665
|
const labelStyle = (
|
|
@@ -675,7 +675,7 @@
|
|
|
675
675
|
T${tileIndex + 1}
|
|
676
676
|
</div>
|
|
677
677
|
<div class="tile-grid" style="${gridStyle}">
|
|
678
|
-
${
|
|
678
|
+
${zonesHtml}
|
|
679
679
|
</div>
|
|
680
680
|
</div>
|
|
681
681
|
`;
|
|
@@ -9,7 +9,7 @@ import time
|
|
|
9
9
|
from typing import Any
|
|
10
10
|
|
|
11
11
|
from lifx_emulator.constants import LIFX_HEADER_SIZE
|
|
12
|
-
from lifx_emulator.devices.states import DeviceState
|
|
12
|
+
from lifx_emulator.devices.states import DeviceState, TileFramebuffers
|
|
13
13
|
from lifx_emulator.handlers import HandlerRegistry, create_default_registry
|
|
14
14
|
from lifx_emulator.protocol.header import LifxHeader
|
|
15
15
|
from lifx_emulator.protocol.packets import (
|
|
@@ -90,10 +90,10 @@ class EmulatedLifxDevice:
|
|
|
90
90
|
if self.state.has_matrix and self.state.tile_count > 0:
|
|
91
91
|
if not self.state.tile_devices:
|
|
92
92
|
for i in range(self.state.tile_count):
|
|
93
|
-
|
|
93
|
+
zones = self.state.tile_width * self.state.tile_height
|
|
94
94
|
tile_colors = [
|
|
95
95
|
LightHsbk(hue=0, saturation=0, brightness=32768, kelvin=3500)
|
|
96
|
-
for _ in range(
|
|
96
|
+
for _ in range(zones)
|
|
97
97
|
]
|
|
98
98
|
|
|
99
99
|
self.state.tile_devices.append(
|
|
@@ -114,6 +114,12 @@ class EmulatedLifxDevice:
|
|
|
114
114
|
}
|
|
115
115
|
)
|
|
116
116
|
|
|
117
|
+
# Initialize framebuffer storage for each tile (framebuffers 1-7)
|
|
118
|
+
# Framebuffer 0 is stored in tile_devices[i]["colors"]
|
|
119
|
+
if not self.state.tile_framebuffers:
|
|
120
|
+
for i in range(self.state.tile_count):
|
|
121
|
+
self.state.tile_framebuffers.append(TileFramebuffers(tile_index=i))
|
|
122
|
+
|
|
117
123
|
# Save initial state if persistence is enabled
|
|
118
124
|
# This ensures newly created devices are immediately persisted
|
|
119
125
|
if self.storage:
|