Publish LumaOps source
This commit is contained in:
@@ -0,0 +1,240 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import struct
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from lumaops_backend.config import Settings
|
||||
from lumaops_backend.connectors.base import DeviceState, RGBColor
|
||||
from lumaops_backend.connectors.openrgb.adapter import OpenRGBAdapter, expand_colors
|
||||
from lumaops_backend.connectors.openrgb.protocol import (
|
||||
HEADER,
|
||||
ModeFlag,
|
||||
PacketId,
|
||||
pack_color,
|
||||
pack_header,
|
||||
pack_string,
|
||||
parse_header,
|
||||
)
|
||||
|
||||
|
||||
def controller_packet(
|
||||
*,
|
||||
active_mode: int = 0,
|
||||
controller_colors: list[RGBColor] | None = None,
|
||||
static_color: RGBColor | None = None,
|
||||
) -> bytes:
|
||||
color = static_color or RGBColor(red=16, green=32, blue=48)
|
||||
led_colors = controller_colors or [RGBColor(red=16, green=32, blue=48)] * 2
|
||||
direct_mode = (
|
||||
pack_string("Direct")
|
||||
+ struct.pack("<iI", 0, int(ModeFlag.HAS_PER_LED_COLOR))
|
||||
+ struct.pack("<IIIIIIIIII", 0, 0, 0, 0, 0, 0, 0, 0, 0, 1)
|
||||
+ struct.pack("<H", 0)
|
||||
)
|
||||
static_mode = (
|
||||
pack_string("Static")
|
||||
+ struct.pack("<iI", 7, int(ModeFlag.HAS_BRIGHTNESS | ModeFlag.HAS_MODE_SPECIFIC_COLOR))
|
||||
+ struct.pack("<IIIIIIIIII", 0, 0, 0, 255, 1, 1, 0, 128, 0, 1)
|
||||
+ struct.pack("<H", 1)
|
||||
+ pack_color(color)
|
||||
)
|
||||
custom_mode = (
|
||||
pack_string("Custom")
|
||||
+ struct.pack("<iI", 1, int(ModeFlag.HAS_PER_LED_COLOR | ModeFlag.AUTOMATIC_SAVE))
|
||||
+ struct.pack("<IIIIIIIIII", 0, 0, 0, 0, 0, 0, 0, 0, 0, 1)
|
||||
+ struct.pack("<H", 0)
|
||||
)
|
||||
zone = (
|
||||
pack_string("Main")
|
||||
+ struct.pack("<iIIIH", 1, 0, 120, 2, 0)
|
||||
+ struct.pack("<H", 0)
|
||||
+ struct.pack("<I", 0)
|
||||
)
|
||||
leds = pack_string("LED 1") + struct.pack("<I", 1) + pack_string("LED 2") + struct.pack("<I", 2)
|
||||
body = (
|
||||
struct.pack("<i", 0)
|
||||
+ pack_string("SDK Fixture")
|
||||
+ pack_string("LumaOps")
|
||||
+ pack_string("Integration test")
|
||||
+ pack_string("1.2.3")
|
||||
+ pack_string("SERIAL-1")
|
||||
+ pack_string("usb:1-2")
|
||||
+ struct.pack("<H", 3)
|
||||
+ struct.pack("<i", active_mode)
|
||||
+ direct_mode
|
||||
+ custom_mode
|
||||
+ static_mode
|
||||
+ struct.pack("<H", 1)
|
||||
+ zone
|
||||
+ struct.pack("<H", 2)
|
||||
+ leds
|
||||
+ struct.pack("<H", 2)
|
||||
+ b"".join(pack_color(item) for item in led_colors)
|
||||
+ struct.pack("<H", 0)
|
||||
+ struct.pack("<I", 1)
|
||||
)
|
||||
return struct.pack("<I", len(body) + 4) + body
|
||||
|
||||
|
||||
def test_uniform_color_state_survives_a_changed_led_count() -> None:
|
||||
color = RGBColor(red=86, green=96, blue=255)
|
||||
|
||||
assert expand_colors([color] * 6, 23) == [color] * 23
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_real_adapter_handshake_inventory_and_write(tmp_path: Path) -> None:
|
||||
seen: list[tuple[int, bytes]] = []
|
||||
update_received = asyncio.Event()
|
||||
effect_received = asyncio.Event()
|
||||
resize_received = asyncio.Event()
|
||||
active_mode = 0
|
||||
fixture_colors = [RGBColor(red=16, green=32, blue=48)] * 2
|
||||
fixture_static_color = RGBColor(red=16, green=32, blue=48)
|
||||
|
||||
async def handle(reader: asyncio.StreamReader, writer: asyncio.StreamWriter) -> None:
|
||||
nonlocal active_mode, fixture_colors, fixture_static_color
|
||||
try:
|
||||
while True:
|
||||
header = parse_header(await reader.readexactly(HEADER.size))
|
||||
payload = await reader.readexactly(header.payload_size)
|
||||
seen.append((header.packet_id, payload))
|
||||
if header.packet_id == PacketId.UPDATE_LEDS:
|
||||
count = struct.unpack_from("<H", payload, 4)[0]
|
||||
fixture_colors = []
|
||||
for index in range(count):
|
||||
red, green, blue = struct.unpack_from("<BBB", payload, 6 + index * 4)
|
||||
fixture_colors.append(RGBColor(red=red, green=green, blue=blue))
|
||||
update_received.set()
|
||||
elif header.packet_id == PacketId.UPDATE_ZONE_LEDS:
|
||||
zone_index, count = struct.unpack_from("<IH", payload, 4)
|
||||
assert zone_index == 0
|
||||
fixture_colors = []
|
||||
for index in range(count):
|
||||
red, green, blue = struct.unpack_from("<BBB", payload, 10 + index * 4)
|
||||
fixture_colors.append(RGBColor(red=red, green=green, blue=blue))
|
||||
update_received.set()
|
||||
elif header.packet_id == PacketId.UPDATE_MODE:
|
||||
active_mode = struct.unpack_from("<i", payload, 4)[0]
|
||||
if active_mode == 2:
|
||||
red, green, blue = struct.unpack_from("<BBB", payload, len(payload) - 4)
|
||||
fixture_static_color = RGBColor(red=red, green=green, blue=blue)
|
||||
effect_received.set()
|
||||
elif header.packet_id == PacketId.SET_CUSTOM_MODE:
|
||||
active_mode = 0
|
||||
elif header.packet_id == PacketId.RESIZE_ZONE:
|
||||
resize_received.set()
|
||||
response: bytes | None = None
|
||||
if header.packet_id == PacketId.REQUEST_PROTOCOL_VERSION:
|
||||
response = struct.pack("<I", 5)
|
||||
elif header.packet_id == PacketId.REQUEST_CONTROLLER_COUNT:
|
||||
response = struct.pack("<I", 1)
|
||||
elif header.packet_id == PacketId.REQUEST_CONTROLLER_DATA:
|
||||
response = controller_packet(
|
||||
active_mode=active_mode,
|
||||
controller_colors=fixture_colors,
|
||||
static_color=fixture_static_color,
|
||||
)
|
||||
if response is not None:
|
||||
writer.write(
|
||||
pack_header(header.device_index, header.packet_id, len(response)) + response
|
||||
)
|
||||
await writer.drain()
|
||||
except asyncio.IncompleteReadError:
|
||||
pass
|
||||
finally:
|
||||
writer.close()
|
||||
await writer.wait_closed()
|
||||
|
||||
server = await asyncio.start_server(handle, "127.0.0.1", 0)
|
||||
port = server.sockets[0].getsockname()[1]
|
||||
settings = Settings(
|
||||
LUMAOPS_ENV="test",
|
||||
auth_enabled=False,
|
||||
openrgb_host="127.0.0.1",
|
||||
openrgb_port=port,
|
||||
openrgb_connect_timeout=1,
|
||||
openrgb_command_timeout=1,
|
||||
config_dir=tmp_path / "config",
|
||||
openrgb_config_dir=tmp_path / "openrgb",
|
||||
data_dir=tmp_path / "data",
|
||||
logs_dir=tmp_path / "logs",
|
||||
database_url=f"sqlite:///{tmp_path / 'test.db'}",
|
||||
)
|
||||
adapter = OpenRGBAdapter(settings)
|
||||
try:
|
||||
devices = await adapter.inventory()
|
||||
assert len(devices) == 1
|
||||
assert devices[0].name == "SDK Fixture"
|
||||
state = await adapter.set_state(
|
||||
devices[0].external_id,
|
||||
DeviceState(
|
||||
colors=[RGBColor(red=2, green=4, blue=8)],
|
||||
brightness=100,
|
||||
mode_index=1,
|
||||
),
|
||||
)
|
||||
assert state.colors == [RGBColor(red=2, green=4, blue=8)] * 2
|
||||
assert state.mode == "Direct"
|
||||
assert state.mode_index == 0
|
||||
assert state.brightness is None
|
||||
await adapter.resize_zone(devices[0].external_id, 0, 24)
|
||||
await asyncio.wait_for(resize_received.wait(), timeout=1)
|
||||
assert [packet_id for packet_id, _payload in seen[:4]] == [
|
||||
PacketId.REQUEST_PROTOCOL_VERSION,
|
||||
PacketId.SET_CLIENT_NAME,
|
||||
PacketId.REQUEST_CONTROLLER_COUNT,
|
||||
PacketId.REQUEST_CONTROLLER_DATA,
|
||||
]
|
||||
assert any(packet_id == PacketId.SET_CUSTOM_MODE for packet_id, _payload in seen)
|
||||
assert any(packet_id == PacketId.UPDATE_LEDS for packet_id, _payload in seen)
|
||||
assert (PacketId.RESIZE_ZONE, struct.pack("<ii", 0, 24)) in seen
|
||||
|
||||
zone_start = len(seen)
|
||||
zone_color = RGBColor(red=9, green=18, blue=27)
|
||||
zone_state = await adapter.set_state(
|
||||
devices[0].external_id,
|
||||
DeviceState(colors=[zone_color], zone_index=0),
|
||||
)
|
||||
zone_packets = [packet_id for packet_id, _payload in seen[zone_start:]]
|
||||
assert zone_state.colors == [zone_color] * 2
|
||||
assert PacketId.UPDATE_ZONE_LEDS in zone_packets
|
||||
assert PacketId.SET_CUSTOM_MODE not in zone_packets
|
||||
|
||||
effect_start = len(seen)
|
||||
effect_received.clear()
|
||||
effect_state = await adapter.set_state(
|
||||
devices[0].external_id,
|
||||
DeviceState(
|
||||
colors=[RGBColor(red=90, green=45, blue=180)],
|
||||
brightness=80,
|
||||
mode_index=2,
|
||||
),
|
||||
)
|
||||
await asyncio.wait_for(effect_received.wait(), timeout=1)
|
||||
effect_packets = [packet_id for packet_id, _payload in seen[effect_start:]]
|
||||
assert effect_state.mode == "Static"
|
||||
assert effect_state.mode_index == 2
|
||||
assert effect_state.colors == [RGBColor(red=90, green=45, blue=180)]
|
||||
assert PacketId.UPDATE_MODE in effect_packets
|
||||
assert PacketId.SET_CUSTOM_MODE not in effect_packets
|
||||
assert PacketId.UPDATE_LEDS not in effect_packets
|
||||
|
||||
direct_start = len(seen)
|
||||
direct_color = RGBColor(red=24, green=48, blue=96)
|
||||
direct_state = await adapter.set_state(
|
||||
devices[0].external_id,
|
||||
DeviceState(colors=[direct_color]),
|
||||
)
|
||||
direct_packets = [packet_id for packet_id, _payload in seen[direct_start:]]
|
||||
assert direct_state.mode == "Direct"
|
||||
assert direct_state.colors == [direct_color] * 2
|
||||
assert PacketId.SET_CUSTOM_MODE in direct_packets
|
||||
assert PacketId.UPDATE_LEDS in direct_packets
|
||||
finally:
|
||||
await adapter.stop()
|
||||
server.close()
|
||||
await server.wait_closed()
|
||||
Reference in New Issue
Block a user