feat: integrate NFC infrared peripherals and orchestration

This commit is contained in:
vee
2026-08-25 11:45:45 +08:00
parent 9ac8fdface
commit 2529b4896a
19 changed files with 3818 additions and 117 deletions
+141
View File
@@ -0,0 +1,141 @@
from __future__ import annotations
import time
from dataclasses import dataclass
from cognitive_agent.infrastructure.nfc.reader import NfcReaderService
@dataclass
class FakePort:
device: str
description: str = ""
hwid: str = ""
class FakeReader:
def __init__(self, port: str) -> None:
self.port = port
self.closed = False
self.sam_configured = False
def close(self) -> None:
self.closed = True
def get_firmware_version(self) -> tuple[int, int]:
return (1, 6)
def sam_configuration(self) -> bool:
self.sam_configured = True
return True
def read_uid(self) -> bytes:
return bytes.fromhex("04A1B2C3")
def wait_until(predicate, timeout: float = 1.0) -> bool:
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
if predicate():
return True
time.sleep(0.01)
return predicate()
def test_resolve_port_prefers_a_single_ch340_reader() -> None:
ports = [
FakePort("COM3", "Bluetooth serial"),
FakePort("COM5", "USB-SERIAL CH340", "USB VID:PID=1A86:7523"),
]
assert NfcReaderService.resolve_port("auto", ports) == "COM5"
assert NfcReaderService.resolve_port("COM8", ports) == "COM8"
def test_reader_service_publishes_reader_and_card_state() -> None:
created: list[FakeReader] = []
def create_reader(port: str) -> FakeReader:
reader = FakeReader(port)
created.append(reader)
return reader
service = NfcReaderService(
reader_factory=create_reader,
ports_provider=lambda: [FakePort("COM5", "PCR532 CH340")],
retry_seconds=0.01,
poll_seconds=0.01,
mqtt_enabled=False,
)
service.start()
try:
assert wait_until(lambda: service.snapshot()["card"] is not None)
snapshot = service.snapshot()
assert snapshot["reader"] == {
"online": True,
"state": "ready",
"port": "COM5",
"firmware": "1.6",
"error": None,
"source": "serial",
}
assert snapshot["card"]["uid"] == "04A1B2C3"
assert snapshot["card"]["action"] == "present"
assert created[0].sam_configured is True
finally:
service.close()
assert created[0].closed is True
def test_disabled_reader_is_non_blocking_and_reported() -> None:
service = NfcReaderService(enabled=False, mqtt_enabled=False)
service.start()
snapshot = service.snapshot()
assert snapshot["reader"]["online"] is False
assert snapshot["reader"]["state"] == "disabled"
assert snapshot["card"] is None
def test_existing_mqtt_bridge_is_reused_when_it_owns_the_reader() -> None:
service = NfcReaderService(enabled=True, mqtt_enabled=False)
service._handle_bridge_status(
{"online": True, "state": "ready", "port": "COM5", "firmware": "1.6"}
)
service._handle_bridge_card(
{"action": "present", "uid": "1a53dd95"}
)
service._handle_bridge_profiles(
{
"profiles": [
{
"id": "ambient-light",
"name": "第三方氛围灯",
"cardUid": "1a53dd95",
"actions": [
{"id": "red", "slot": 7, "name": "红色", "learned": True}
],
}
]
}
)
service._handle_local_bridge_status({"addresses": ["172.20.10.8"]})
snapshot = service.snapshot()
assert snapshot["reader"] == {
"online": True,
"state": "ready",
"port": "COM5",
"firmware": "1.6",
"error": None,
"source": "mqtt_bridge",
}
assert snapshot["card"]["uid"] == "1A53DD95"
assert snapshot["legacy_profiles"][0]["name"] == "第三方氛围灯"
assert snapshot["legacy_profiles"][0]["cardUid"] == "1A53DD95"
assert snapshot["legacy_profiles"][0]["actions"][0]["slot"] == 7
assert snapshot["legacy_bridge_addresses"] == ["172.20.10.8"]
service._handle_bridge_card({"action": "removed", "uid": "1A53DD95"})
assert service.snapshot()["card"] is None