Packet Flow¶
How UDP packets are received, routed, processed, and responded to.
Overview¶
Every interaction with the emulator follows the same path:
- UDP packet arrives at
EmulatedLifxServer - Header is parsed to determine target device(s)
- Acknowledgment sent immediately (unless a scenario targets acks)
- Payload is unpacked into a typed packet object
- Device processes the packet through its handler registry
- Scenarios are applied (drops, delays, malformed responses)
- Response packets are packed and sent back via UDP
Step-by-Step Flow¶
1. Reception¶
The server listens on a UDP socket via asyncio.DatagramProtocol. When bytes arrive:
The 36-byte header is parsed first. If the header is malformed or too short, the packet is silently dropped (matching real LIFX device behavior).
2. Payload Unpacking¶
The pkt_type field from the header determines which packet class to use:
Packet classes are organized by protocol namespace:
Device.*— types 2-59 (discovery, identity, power)Light.*— types 101-149 (color, waveform, infrared, HEV)MultiZone.*— types 501-512 (zone colors, effects)Tile.*— types 701-720 (matrix colors, effects, framebuffers)
3. Acknowledgment Handling¶
If ack_required=True in the request header and no scenario targets ack behavior, the server sends an Acknowledgment packet (type 45) immediately via UDP before routing the packet to device handlers. This ensures the client receives the ack with minimal latency.
When a scenario does target acks (e.g., delaying, dropping, or corrupting type 45), the server defers to the device, which includes the ack in its response list for scenario processing.
4. Device Routing¶
The header's target field (6-byte MAC + 2 null bytes) determines which device(s) receive the packet:
- Broadcast (
tagged=Trueor target is all zeros): packet forwarded to every device - Unicast (
tagged=Falsewith specific target): routed to the device matching the serial encoded in the target field - Unknown target: packet silently dropped
5. Packet Processing¶
Each device processes the packet through EmulatedLifxDevice.process_packet():
process_packet(header, packet)
├── Look up handler in HandlerRegistry by pkt_type
├── If no handler found → return empty (or StateUnhandled for switches)
├── Apply scenario: check for packet drops
├── Call handler.handle(device_state, packet, res_required)
├── Handler reads/updates state and returns response packet(s)
├── Apply scenario: response delays, malformed packets, partial responses
├── Construct response headers
└── Return list of (header, packet) tuples
6. Handler Dispatch¶
The HandlerRegistry maps packet type numbers to handler instances using the Strategy pattern:
handler = registry.get_handler(pkt_type) # e.g., 101 → GetColorHandler
response = handler.handle(device_state, packet, res_required)
Handlers are split across four modules matching the protocol namespaces:
| Module | Packet Types | Examples |
|---|---|---|
device_handlers.py |
2-59 | GetService, GetVersion, SetLabel |
light_handlers.py |
101-149 | GetColor, SetColor, GetInfrared |
multizone_handlers.py |
501-512 | GetColorZones, SetExtendedColorZones |
tile_handlers.py |
701-720 | Get64, Set64, CopyFrameBuffer |
7. Response Construction¶
Handlers return packet objects (not headers). The device wraps each response in a header:
sourceandsequenceare copied from the request headertargetis set to the device's own serialpkt_typeis set to the response packet's typesizeis calculated from header + payload length
8. Multi-Packet Responses¶
Some handlers return multiple packets:
- Multizone (
GetColorZones): oneStateMultiZone(type 506) per 8 zones - Extended multizone (
GetExtendedColorZones): one or moreExtendedStateMultiZone(type 512) per 82 zones - Tile (
Get64): oneStateTileState64per tile in the chain, with up to 64 zones per packet
Handlers return these as lists, and process_packet() constructs a separate header for each.
9. Sending¶
The server packs each (header, packet) tuple to bytes and sends via UDP back to the client's address and port.
Scenario Interception Points¶
Scenarios modify the flow at several points during process_packet():
| Scenario | When Applied | Effect |
|---|---|---|
drop_packets |
Before handler dispatch | Packet silently dropped (no response) |
response_delays |
After handler returns | asyncio.sleep() before sending |
malformed_packets |
After handler returns | Response payload truncated/corrupted |
invalid_field_values |
After handler returns | All response bytes set to 0xFF |
partial_responses |
After handler returns | Multi-packet response list randomly truncated |
send_unhandled |
When no handler found | Forces StateUnhandled (type 223) response |
firmware_version |
During state reads | Overrides reported firmware version |
Switch Device Behavior¶
Switch devices (has_relays=True) handle Device namespace packets (types 2-59) normally but return StateUnhandled (type 223) for Light, MultiZone, and Tile packets. This matches physical LIFX Switch behavior.
See Also¶
- Architecture Overview — High-level component diagram
- Protocol Layer — Header and packet structure details
- Device State — How state is read and updated by handlers