feat: integrate learned peripherals into mode creation
Expose safe NFC-bound infrared actions to semantic scene composition, require flashing-light actions for romantic and Mid-Autumn modes, and preserve mixed CAN/IR mode flows. Fix NFC device reassignment and retry extension/toy MQTT publication when an active card's binding catalog changes.
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import threading
|
||||
import time
|
||||
@@ -121,13 +122,14 @@ def build_extension_toy_messages(
|
||||
|
||||
|
||||
class NfcExtensionToyPublisher:
|
||||
"""Publish one three-frame ability burst for each NFC card-detection edge."""
|
||||
"""Publish one three-frame burst per NFC edge or active-card binding change."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
nfc_provider: Callable[[], Mapping[str, Any]],
|
||||
catalog_provider: Callable[[], list[dict[str, Any]]],
|
||||
catalog_revision_provider: Callable[[], object] | None = None,
|
||||
publisher: Callable[[Mapping[str, Any]], Mapping[str, Any]],
|
||||
burst_count: int = EXTENSION_TOY_BURST_COUNT,
|
||||
burst_window_s: float = EXTENSION_TOY_BURST_WINDOW_S,
|
||||
@@ -148,6 +150,7 @@ class NfcExtensionToyPublisher:
|
||||
self.poll_interval_s = float(poll_interval_s)
|
||||
self._nfc_provider = nfc_provider
|
||||
self._catalog_provider = catalog_provider
|
||||
self._catalog_revision_provider = catalog_revision_provider
|
||||
self._publisher = publisher
|
||||
self._lock = threading.RLock()
|
||||
self._stop = threading.Event()
|
||||
@@ -161,6 +164,7 @@ class NfcExtensionToyPublisher:
|
||||
"burst_count": self.burst_count,
|
||||
"burst_window_ms": round(self.burst_window_s * 1_000),
|
||||
"active_nfc_id": None,
|
||||
"active_catalog_revision": None,
|
||||
"active_extension_names": [],
|
||||
"active_target_scopes": [],
|
||||
"message_count": 0,
|
||||
@@ -185,6 +189,7 @@ class NfcExtensionToyPublisher:
|
||||
self._set_status(
|
||||
state="waiting_for_nfc",
|
||||
active_nfc_id=None,
|
||||
active_catalog_revision=None,
|
||||
active_extension_names=[],
|
||||
active_target_scopes=[],
|
||||
message_count=0,
|
||||
@@ -206,6 +211,7 @@ class NfcExtensionToyPublisher:
|
||||
self._set_status(
|
||||
state="stopped",
|
||||
active_nfc_id=None,
|
||||
active_catalog_revision=None,
|
||||
active_extension_names=[],
|
||||
active_target_scopes=[],
|
||||
message_count=0,
|
||||
@@ -234,6 +240,11 @@ class NfcExtensionToyPublisher:
|
||||
return ""
|
||||
return normalize_extension_nfc_id(card.get("uid"))
|
||||
|
||||
def _catalog_revision(self) -> str:
|
||||
if self._catalog_revision_provider is None:
|
||||
return ""
|
||||
return str(self._catalog_revision_provider() or "").strip()[:128]
|
||||
|
||||
def _publish_burst(
|
||||
self,
|
||||
card_uid: str,
|
||||
@@ -325,6 +336,8 @@ class NfcExtensionToyPublisher:
|
||||
|
||||
def _run(self) -> None:
|
||||
latched_uid = ""
|
||||
latched_catalog_revision = ""
|
||||
latched_message_signature: str | None = None
|
||||
while not self._stop.is_set():
|
||||
try:
|
||||
card_uid = self._active_nfc_id()
|
||||
@@ -333,12 +346,32 @@ class NfcExtensionToyPublisher:
|
||||
self._stop.wait(self.poll_interval_s)
|
||||
continue
|
||||
|
||||
if card_uid != latched_uid:
|
||||
try:
|
||||
catalog_revision = self._catalog_revision() if card_uid else ""
|
||||
except Exception as exc: # noqa: BLE001 - retry while the card remains
|
||||
self._set_status(
|
||||
state="catalog_unavailable",
|
||||
active_nfc_id=card_uid or None,
|
||||
last_error=str(exc)[:240],
|
||||
)
|
||||
self._stop.wait(self.poll_interval_s)
|
||||
continue
|
||||
|
||||
catalog_changed = bool(
|
||||
card_uid
|
||||
and card_uid == latched_uid
|
||||
and self._catalog_revision_provider is not None
|
||||
and catalog_revision != latched_catalog_revision
|
||||
)
|
||||
if card_uid != latched_uid or catalog_changed:
|
||||
if not card_uid:
|
||||
latched_uid = ""
|
||||
latched_catalog_revision = ""
|
||||
latched_message_signature = None
|
||||
self._set_status(
|
||||
state="waiting_for_nfc",
|
||||
active_nfc_id=None,
|
||||
active_catalog_revision=None,
|
||||
active_extension_names=[],
|
||||
active_target_scopes=[],
|
||||
message_count=0,
|
||||
@@ -358,10 +391,28 @@ class NfcExtensionToyPublisher:
|
||||
last_error=str(exc)[:240],
|
||||
)
|
||||
else:
|
||||
message_signature = json.dumps(
|
||||
messages,
|
||||
ensure_ascii=False,
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
)
|
||||
messages_changed = (
|
||||
message_signature != latched_message_signature
|
||||
)
|
||||
# Latch only after a successful catalog read. A card that
|
||||
# remains present never starts a second burst.
|
||||
# remains present publishes again only when the abilities
|
||||
# bound to that exact UID changed. Unrelated catalog
|
||||
# revisions therefore do not duplicate a burst.
|
||||
latched_uid = card_uid
|
||||
if messages:
|
||||
latched_catalog_revision = catalog_revision
|
||||
latched_message_signature = message_signature
|
||||
self._set_status(
|
||||
active_catalog_revision=catalog_revision or None,
|
||||
)
|
||||
if not messages_changed:
|
||||
pass
|
||||
elif messages:
|
||||
self._publish_burst(card_uid, messages)
|
||||
else:
|
||||
self._set_status(
|
||||
|
||||
Reference in New Issue
Block a user