lifx-emulator 2.2.1__tar.gz → 2.3.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_emulator-2.2.1 → lifx_emulator-2.3.0}/CLAUDE.md +16 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/PKG-INFO +1 -1
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/docs/changelog.md +8 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/docs/guide/device-types.md +11 -0
- lifx_emulator-2.3.0/docs/guide/framebuffers.md +209 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/pyproject.toml +2 -1
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/src/lifx_emulator/devices/device.py +7 -1
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/src/lifx_emulator/devices/state_serializer.py +27 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/src/lifx_emulator/devices/states.py +31 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/src/lifx_emulator/handlers/tile_handlers.py +142 -15
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/tests/test_tile_handlers_extended.py +295 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/uv.lock +1 -1
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/.github/workflows/ci.yml +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/.github/workflows/docs.yml +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/.gitignore +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/.pre-commit-config.yaml +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/LICENSE +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/README.md +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/docs/advanced/device-management-api.md +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/docs/advanced/index.md +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/docs/advanced/scenario-api.md +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/docs/advanced/scenarios.md +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/docs/advanced/storage.md +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/docs/api/device.md +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/docs/api/factories.md +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/docs/api/index.md +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/docs/api/products.md +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/docs/api/protocol.md +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/docs/api/server.md +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/docs/api/storage.md +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/docs/architecture/device-state.md +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/docs/architecture/index.md +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/docs/architecture/overview.md +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/docs/architecture/packet-flow.md +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/docs/architecture/protocol.md +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/docs/assets/favicon.png +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/docs/faq.md +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/docs/getting-started/cli.md +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/docs/getting-started/index.md +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/docs/getting-started/installation.md +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/docs/getting-started/quickstart.md +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/docs/guide/best-practices.md +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/docs/guide/index.md +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/docs/guide/integration-testing.md +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/docs/guide/products-and-specs.md +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/docs/guide/testing-scenarios.md +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/docs/guide/web-interface.md +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/docs/index.md +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/docs/reference/glossary.md +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/docs/reference/troubleshooting.md +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/docs/stylesheets/extra.css +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/docs/tutorials/01-first-device.md +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/docs/tutorials/02-basic.md +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/docs/tutorials/03-integration.md +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/docs/tutorials/04-advanced-scenarios.md +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/docs/tutorials/05-cicd.md +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/docs/tutorials/index.md +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/mkdocs.yml +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/renovate.json +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/src/lifx_emulator/__init__.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/src/lifx_emulator/__main__.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/src/lifx_emulator/api/__init__.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/src/lifx_emulator/api/app.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/src/lifx_emulator/api/mappers/__init__.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/src/lifx_emulator/api/mappers/device_mapper.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/src/lifx_emulator/api/models.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/src/lifx_emulator/api/routers/__init__.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/src/lifx_emulator/api/routers/devices.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/src/lifx_emulator/api/routers/monitoring.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/src/lifx_emulator/api/routers/scenarios.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/src/lifx_emulator/api/services/__init__.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/src/lifx_emulator/api/services/device_service.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/src/lifx_emulator/api/templates/dashboard.html +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/src/lifx_emulator/constants.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/src/lifx_emulator/devices/__init__.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/src/lifx_emulator/devices/manager.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/src/lifx_emulator/devices/observers.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/src/lifx_emulator/devices/persistence.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/src/lifx_emulator/devices/state_restorer.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/src/lifx_emulator/factories/__init__.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/src/lifx_emulator/factories/builder.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/src/lifx_emulator/factories/default_config.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/src/lifx_emulator/factories/factory.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/src/lifx_emulator/factories/firmware_config.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/src/lifx_emulator/factories/serial_generator.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/src/lifx_emulator/handlers/__init__.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/src/lifx_emulator/handlers/base.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/src/lifx_emulator/handlers/device_handlers.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/src/lifx_emulator/handlers/light_handlers.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/src/lifx_emulator/handlers/multizone_handlers.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/src/lifx_emulator/handlers/registry.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/src/lifx_emulator/products/__init__.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/src/lifx_emulator/products/generator.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/src/lifx_emulator/products/registry.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/src/lifx_emulator/products/specs.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/src/lifx_emulator/products/specs.yml +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/src/lifx_emulator/protocol/__init__.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/src/lifx_emulator/protocol/base.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/src/lifx_emulator/protocol/const.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/src/lifx_emulator/protocol/generator.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/src/lifx_emulator/protocol/header.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/src/lifx_emulator/protocol/packets.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/src/lifx_emulator/protocol/protocol_types.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/src/lifx_emulator/protocol/serializer.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/src/lifx_emulator/repositories/__init__.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/src/lifx_emulator/repositories/device_repository.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/src/lifx_emulator/repositories/storage_backend.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/src/lifx_emulator/scenarios/__init__.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/src/lifx_emulator/scenarios/manager.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/src/lifx_emulator/scenarios/models.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/src/lifx_emulator/scenarios/persistence.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/src/lifx_emulator/server.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/tests/conftest.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/tests/test_api.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/tests/test_api_validation.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/tests/test_async_storage.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/tests/test_cli.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/tests/test_cli_validation.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/tests/test_device.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/tests/test_device_edge_cases.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/tests/test_device_handlers_extended.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/tests/test_device_manager.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/tests/test_handler_registry.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/tests/test_integration.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/tests/test_light_handlers_extended.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/tests/test_multizone_handlers_extended.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/tests/test_observers.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/tests/test_products_generator.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/tests/test_products_specs.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/tests/test_protocol_generator.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/tests/test_protocol_types_coverage.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/tests/test_repositories.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/tests/test_scenario_manager.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/tests/test_scenario_persistence.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/tests/test_serializer.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/tests/test_server.py +0 -0
- {lifx_emulator-2.2.1 → lifx_emulator-2.3.0}/tests/test_state_restorer.py +0 -0
|
@@ -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)
|
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
<!-- version list -->
|
|
4
4
|
|
|
5
|
+
## v2.3.0 (2025-11-18)
|
|
6
|
+
|
|
7
|
+
### Features
|
|
8
|
+
|
|
9
|
+
- **tile**: Implement multi-framebuffer support for matrix devices
|
|
10
|
+
([`16a69ca`](https://github.com/Djelibeybi/lifx-emulator/commit/16a69cacbdf0472d8f7116eb0acfda9808431c5c))
|
|
11
|
+
|
|
12
|
+
|
|
5
13
|
## v2.2.1 (2025-11-18)
|
|
6
14
|
|
|
7
15
|
### Bug Fixes
|
|
@@ -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
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "lifx-emulator"
|
|
3
|
-
version = "2.
|
|
3
|
+
version = "2.3.0"
|
|
4
4
|
description = "LIFX Emulator for testing LIFX LAN protocol libraries"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.11"
|
|
@@ -88,6 +88,7 @@ max-statements = 50
|
|
|
88
88
|
[tool.ruff.lint.per-file-ignores]
|
|
89
89
|
"src/lifx_emulator/__main__.py" = ["C901"]
|
|
90
90
|
"src/lifx_emulator/devices/state_restorer.py" = ["C901"]
|
|
91
|
+
"src/lifx_emulator/handlers/tile_handlers.py" = ["C901"]
|
|
91
92
|
"src/lifx_emulator/protocol/generator.py" = ["E501", "C901"]
|
|
92
93
|
"src/lifx_emulator/protocol/packets.py" = ["E501"]
|
|
93
94
|
"src/lifx_emulator/products/registry.py" = ["C901"]
|
|
@@ -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 (
|
|
@@ -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:
|
|
@@ -97,6 +97,17 @@ def serialize_device_state(device_state: Any) -> dict[str, Any]:
|
|
|
97
97
|
}
|
|
98
98
|
for t in device_state.tile_devices
|
|
99
99
|
]
|
|
100
|
+
# Serialize tile framebuffers (non-visible framebuffers 1-7)
|
|
101
|
+
state_dict["tile_framebuffers"] = [
|
|
102
|
+
{
|
|
103
|
+
"tile_index": fb.tile_index,
|
|
104
|
+
"framebuffers": {
|
|
105
|
+
str(fb_idx): [serialize_hsbk(c) for c in colors]
|
|
106
|
+
for fb_idx, colors in fb.framebuffers.items()
|
|
107
|
+
},
|
|
108
|
+
}
|
|
109
|
+
for fb in device_state.tile_framebuffers
|
|
110
|
+
]
|
|
100
111
|
|
|
101
112
|
return state_dict
|
|
102
113
|
|
|
@@ -127,4 +138,20 @@ def deserialize_device_state(state_dict: dict[str, Any]) -> dict[str, Any]:
|
|
|
127
138
|
for tile_dict in state_dict["tile_devices"]:
|
|
128
139
|
tile_dict["colors"] = [deserialize_hsbk(c) for c in tile_dict["colors"]]
|
|
129
140
|
|
|
141
|
+
# Deserialize tile framebuffers if present (for backwards compatibility)
|
|
142
|
+
if "tile_framebuffers" in state_dict:
|
|
143
|
+
from lifx_emulator.devices.states import TileFramebuffers
|
|
144
|
+
|
|
145
|
+
deserialized_fbs = []
|
|
146
|
+
for fb_dict in state_dict["tile_framebuffers"]:
|
|
147
|
+
tile_fb = TileFramebuffers(tile_index=fb_dict["tile_index"])
|
|
148
|
+
# Deserialize each framebuffer's colors
|
|
149
|
+
for fb_idx_str, colors_list in fb_dict["framebuffers"].items():
|
|
150
|
+
fb_idx = int(fb_idx_str)
|
|
151
|
+
tile_fb.framebuffers[fb_idx] = [
|
|
152
|
+
deserialize_hsbk(c) for c in colors_list
|
|
153
|
+
]
|
|
154
|
+
deserialized_fbs.append(tile_fb)
|
|
155
|
+
state_dict["tile_framebuffers"] = deserialized_fbs
|
|
156
|
+
|
|
130
157
|
return state_dict
|
|
@@ -82,6 +82,32 @@ class MultiZoneState:
|
|
|
82
82
|
effect_speed: int = 5 # Duration of one cycle in seconds
|
|
83
83
|
|
|
84
84
|
|
|
85
|
+
@dataclass
|
|
86
|
+
class TileFramebuffers:
|
|
87
|
+
"""Internal storage for non-visible tile framebuffers (1-7).
|
|
88
|
+
|
|
89
|
+
Framebuffer 0 is stored in tile_devices[i]["colors"] (the visible buffer).
|
|
90
|
+
Framebuffers 1-7 are stored here for Set64/CopyFrameBuffer operations.
|
|
91
|
+
Each framebuffer is a list of LightHsbk colors with length = width * height.
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
tile_index: int # Which tile this belongs to
|
|
95
|
+
framebuffers: dict[int, list[LightHsbk]] = field(default_factory=dict)
|
|
96
|
+
|
|
97
|
+
def get_framebuffer(
|
|
98
|
+
self, fb_index: int, width: int, height: int
|
|
99
|
+
) -> list[LightHsbk]:
|
|
100
|
+
"""Get framebuffer by index, creating it if needed."""
|
|
101
|
+
if fb_index not in self.framebuffers:
|
|
102
|
+
# Initialize with default black color
|
|
103
|
+
pixels = width * height
|
|
104
|
+
self.framebuffers[fb_index] = [
|
|
105
|
+
LightHsbk(hue=0, saturation=0, brightness=0, kelvin=3500)
|
|
106
|
+
for _ in range(pixels)
|
|
107
|
+
]
|
|
108
|
+
return self.framebuffers[fb_index]
|
|
109
|
+
|
|
110
|
+
|
|
85
111
|
@dataclass
|
|
86
112
|
class MatrixState:
|
|
87
113
|
"""Matrix (tile/candle) capability state."""
|
|
@@ -101,6 +127,9 @@ class MatrixState:
|
|
|
101
127
|
effect_cloud_sat_max: int = (
|
|
102
128
|
0 # Max cloud saturation 0-200 (only when effect_type=5)
|
|
103
129
|
)
|
|
130
|
+
# Internal storage for non-visible framebuffers (1-7) per tile
|
|
131
|
+
# Framebuffer 0 remains in tile_devices[i]["colors"]
|
|
132
|
+
tile_framebuffers: list[TileFramebuffers] = field(default_factory=list)
|
|
104
133
|
|
|
105
134
|
|
|
106
135
|
@dataclass
|
|
@@ -215,6 +244,7 @@ class DeviceState:
|
|
|
215
244
|
"tile_effect_sky_type": ("matrix", "effect_sky_type"),
|
|
216
245
|
"tile_effect_cloud_sat_min": ("matrix", "effect_cloud_sat_min"),
|
|
217
246
|
"tile_effect_cloud_sat_max": ("matrix", "effect_cloud_sat_max"),
|
|
247
|
+
"tile_framebuffers": "matrix",
|
|
218
248
|
}
|
|
219
249
|
|
|
220
250
|
# Default values for optional state attributes when state object is None
|
|
@@ -240,6 +270,7 @@ class DeviceState:
|
|
|
240
270
|
"tile_effect_sky_type": 0,
|
|
241
271
|
"tile_effect_cloud_sat_min": 0,
|
|
242
272
|
"tile_effect_cloud_sat_max": 0,
|
|
273
|
+
"tile_framebuffers": [],
|
|
243
274
|
}
|
|
244
275
|
|
|
245
276
|
def get_target_bytes(self) -> bytes:
|
|
@@ -12,6 +12,7 @@ from lifx_emulator.protocol.protocol_types import (
|
|
|
12
12
|
DeviceStateVersion,
|
|
13
13
|
LightHsbk,
|
|
14
14
|
TileAccelMeas,
|
|
15
|
+
TileBufferRect,
|
|
15
16
|
TileEffectParameter,
|
|
16
17
|
TileEffectSettings,
|
|
17
18
|
TileEffectType,
|
|
@@ -137,6 +138,10 @@ class Get64Handler(PacketHandler):
|
|
|
137
138
|
tile_width = tile["width"]
|
|
138
139
|
tile_height = tile["height"]
|
|
139
140
|
|
|
141
|
+
# Get64 always returns framebuffer 0 (the visible buffer)
|
|
142
|
+
# regardless of which fb_index is in the request
|
|
143
|
+
tile_colors = tile["colors"]
|
|
144
|
+
|
|
140
145
|
# Calculate how many rows fit in 64 pixels
|
|
141
146
|
rows_to_return = 64 // rect.width if rect.width > 0 else 1
|
|
142
147
|
rows_to_return = min(rows_to_return, tile_height - rect.y)
|
|
@@ -161,8 +166,8 @@ class Get64Handler(PacketHandler):
|
|
|
161
166
|
|
|
162
167
|
# Calculate pixel index in flat color array
|
|
163
168
|
pixel_idx = y * tile_width + x
|
|
164
|
-
if pixel_idx < len(
|
|
165
|
-
colors.append(
|
|
169
|
+
if pixel_idx < len(tile_colors):
|
|
170
|
+
colors.append(tile_colors[pixel_idx])
|
|
166
171
|
else:
|
|
167
172
|
colors.append(
|
|
168
173
|
LightHsbk(hue=0, saturation=0, brightness=0, kelvin=3500)
|
|
@@ -173,7 +178,14 @@ class Get64Handler(PacketHandler):
|
|
|
173
178
|
while len(colors) < 64:
|
|
174
179
|
colors.append(LightHsbk(hue=0, saturation=0, brightness=0, kelvin=3500))
|
|
175
180
|
|
|
176
|
-
|
|
181
|
+
# Return with fb_index forced to 0 (visible buffer)
|
|
182
|
+
return_rect = TileBufferRect(
|
|
183
|
+
fb_index=0, # Always return FB0
|
|
184
|
+
x=rect.x,
|
|
185
|
+
y=rect.y,
|
|
186
|
+
width=rect.width,
|
|
187
|
+
)
|
|
188
|
+
return [Tile.State64(tile_index=tile_index, rect=return_rect, colors=colors)]
|
|
177
189
|
|
|
178
190
|
|
|
179
191
|
class Set64Handler(PacketHandler):
|
|
@@ -188,16 +200,60 @@ class Set64Handler(PacketHandler):
|
|
|
188
200
|
return []
|
|
189
201
|
|
|
190
202
|
tile_index = packet.tile_index
|
|
203
|
+
fb_index = packet.rect.fb_index
|
|
191
204
|
|
|
192
|
-
if tile_index
|
|
193
|
-
|
|
194
|
-
for i, color in enumerate(packet.colors[:64]):
|
|
195
|
-
if i < len(device_state.tile_devices[tile_index]["colors"]):
|
|
196
|
-
device_state.tile_devices[tile_index]["colors"][i] = color
|
|
205
|
+
if tile_index >= len(device_state.tile_devices):
|
|
206
|
+
return []
|
|
197
207
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
208
|
+
tile = device_state.tile_devices[tile_index]
|
|
209
|
+
tile_width = tile["width"]
|
|
210
|
+
tile_height = tile["height"]
|
|
211
|
+
rect = packet.rect
|
|
212
|
+
|
|
213
|
+
# Determine which framebuffer to update
|
|
214
|
+
if fb_index == 0:
|
|
215
|
+
# Update visible framebuffer (stored in tile_devices)
|
|
216
|
+
target_colors = tile["colors"]
|
|
217
|
+
else:
|
|
218
|
+
# Update non-visible framebuffer (stored in tile_framebuffers)
|
|
219
|
+
if tile_index < len(device_state.tile_framebuffers):
|
|
220
|
+
fb_storage = device_state.tile_framebuffers[tile_index]
|
|
221
|
+
target_colors = fb_storage.get_framebuffer(
|
|
222
|
+
fb_index, tile_width, tile_height
|
|
223
|
+
)
|
|
224
|
+
else:
|
|
225
|
+
logger.warning(f"Tile {tile_index} framebuffer storage not initialized")
|
|
226
|
+
return []
|
|
227
|
+
|
|
228
|
+
# Update colors in the specified rectangle
|
|
229
|
+
# Calculate how many rows fit in 64 pixels
|
|
230
|
+
rows_to_write = 64 // rect.width if rect.width > 0 else 1
|
|
231
|
+
rows_to_write = min(rows_to_write, tile_height - rect.y)
|
|
232
|
+
|
|
233
|
+
pixels_written = 0
|
|
234
|
+
for row in range(rows_to_write):
|
|
235
|
+
y = rect.y + row
|
|
236
|
+
if y >= tile_height:
|
|
237
|
+
break
|
|
238
|
+
|
|
239
|
+
for col in range(rect.width):
|
|
240
|
+
x = rect.x + col
|
|
241
|
+
if x >= tile_width or pixels_written >= 64:
|
|
242
|
+
pixels_written += 1
|
|
243
|
+
continue
|
|
244
|
+
|
|
245
|
+
# Calculate pixel index in flat color array
|
|
246
|
+
pixel_idx = y * tile_width + x
|
|
247
|
+
if pixel_idx < len(target_colors) and pixels_written < len(
|
|
248
|
+
packet.colors
|
|
249
|
+
):
|
|
250
|
+
target_colors[pixel_idx] = packet.colors[pixels_written]
|
|
251
|
+
pixels_written += 1
|
|
252
|
+
|
|
253
|
+
logger.info(
|
|
254
|
+
f"Tile {tile_index} FB{fb_index} set {pixels_written} colors at "
|
|
255
|
+
f"({rect.x},{rect.y}), duration={packet.duration}ms"
|
|
256
|
+
)
|
|
201
257
|
|
|
202
258
|
# Tiles never return a response to Set64 regardless of res_required
|
|
203
259
|
# https://lan.developer.lifx.com/docs/changing-a-device#set64---packet-715
|
|
@@ -212,12 +268,83 @@ class CopyFrameBufferHandler(PacketHandler):
|
|
|
212
268
|
def handle(
|
|
213
269
|
self, device_state: DeviceState, packet: Any | None, res_required: bool
|
|
214
270
|
) -> list[Any]:
|
|
215
|
-
if not device_state.has_matrix:
|
|
271
|
+
if not device_state.has_matrix or not packet:
|
|
272
|
+
return []
|
|
273
|
+
|
|
274
|
+
tile_index = packet.tile_index
|
|
275
|
+
if tile_index >= len(device_state.tile_devices):
|
|
216
276
|
return []
|
|
217
277
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
278
|
+
tile = device_state.tile_devices[tile_index]
|
|
279
|
+
tile_width = tile["width"]
|
|
280
|
+
tile_height = tile["height"]
|
|
281
|
+
|
|
282
|
+
src_fb_index = packet.src_fb_index
|
|
283
|
+
dst_fb_index = packet.dst_fb_index
|
|
284
|
+
|
|
285
|
+
# Get source framebuffer
|
|
286
|
+
if src_fb_index == 0:
|
|
287
|
+
src_colors = tile["colors"]
|
|
288
|
+
else:
|
|
289
|
+
if tile_index < len(device_state.tile_framebuffers):
|
|
290
|
+
fb_storage = device_state.tile_framebuffers[tile_index]
|
|
291
|
+
src_colors = fb_storage.get_framebuffer(
|
|
292
|
+
src_fb_index, tile_width, tile_height
|
|
293
|
+
)
|
|
294
|
+
else:
|
|
295
|
+
logger.warning(f"Tile {tile_index} framebuffer storage not initialized")
|
|
296
|
+
return []
|
|
297
|
+
|
|
298
|
+
# Get destination framebuffer
|
|
299
|
+
if dst_fb_index == 0:
|
|
300
|
+
dst_colors = tile["colors"]
|
|
301
|
+
else:
|
|
302
|
+
if tile_index < len(device_state.tile_framebuffers):
|
|
303
|
+
fb_storage = device_state.tile_framebuffers[tile_index]
|
|
304
|
+
dst_colors = fb_storage.get_framebuffer(
|
|
305
|
+
dst_fb_index, tile_width, tile_height
|
|
306
|
+
)
|
|
307
|
+
else:
|
|
308
|
+
logger.warning(f"Tile {tile_index} framebuffer storage not initialized")
|
|
309
|
+
return []
|
|
310
|
+
|
|
311
|
+
# Copy the specified rectangle from source to destination
|
|
312
|
+
src_x = packet.src_x
|
|
313
|
+
src_y = packet.src_y
|
|
314
|
+
dst_x = packet.dst_x
|
|
315
|
+
dst_y = packet.dst_y
|
|
316
|
+
width = packet.width
|
|
317
|
+
height = packet.height
|
|
318
|
+
|
|
319
|
+
pixels_copied = 0
|
|
320
|
+
for row in range(height):
|
|
321
|
+
src_row = src_y + row
|
|
322
|
+
dst_row = dst_y + row
|
|
323
|
+
|
|
324
|
+
if src_row >= tile_height or dst_row >= tile_height:
|
|
325
|
+
break
|
|
326
|
+
|
|
327
|
+
for col in range(width):
|
|
328
|
+
src_col = src_x + col
|
|
329
|
+
dst_col = dst_x + col
|
|
330
|
+
|
|
331
|
+
if src_col >= tile_width or dst_col >= tile_width:
|
|
332
|
+
continue
|
|
333
|
+
|
|
334
|
+
src_idx = src_row * tile_width + src_col
|
|
335
|
+
dst_idx = dst_row * tile_width + dst_col
|
|
336
|
+
|
|
337
|
+
if src_idx < len(src_colors) and dst_idx < len(dst_colors):
|
|
338
|
+
dst_colors[dst_idx] = src_colors[src_idx]
|
|
339
|
+
pixels_copied += 1
|
|
340
|
+
|
|
341
|
+
logger.info(
|
|
342
|
+
f"Tile {tile_index} copied {pixels_copied} pixels from "
|
|
343
|
+
f"FB{src_fb_index}({src_x},{src_y}) to "
|
|
344
|
+
f"FB{dst_fb_index}({dst_x},{dst_y}), "
|
|
345
|
+
f"size={width}x{height}, duration={packet.duration}ms"
|
|
346
|
+
)
|
|
347
|
+
|
|
221
348
|
return []
|
|
222
349
|
|
|
223
350
|
|