fix(web): reveal NFC peripherals after detection

This commit is contained in:
Miayui
2026-09-21 11:04:18 +08:00
parent 448c8b31a0
commit b3bd90f748
9 changed files with 75 additions and 14 deletions
+1 -1
View File
@@ -44,7 +44,7 @@
<script type="module" src="/gesture-control.js?v=android-pad-9"></script> <script type="module" src="/gesture-control.js?v=android-pad-9"></script>
<script src="/pwa.js?v=android-pad-3" defer></script> <script src="/pwa.js?v=android-pad-3" defer></script>
<script src="/console-v2.js?v=local-first-79-cinema-status" defer></script> <script src="/console-v2.js?v=local-first-79-cinema-status" defer></script>
<script src="/self-extension.js?v=nfc-persistent-57" defer></script> <script src="/self-extension.js?v=nfc-session-58" defer></script>
<script src="/assistant-chat.js?v=codex-assistant-39-video-media" defer></script> <script src="/assistant-chat.js?v=codex-assistant-39-video-media" defer></script>
<script src="/energy-monitor.js?v=energy-monitor-5-performance" defer></script> <script src="/energy-monitor.js?v=energy-monitor-5-performance" defer></script>
<script src="/firmware-workbench-entry.js?v=3" defer></script> <script src="/firmware-workbench-entry.js?v=3" defer></script>
+17 -6
View File
@@ -378,6 +378,7 @@
state: "stopped", state: "stopped",
topic: "extension/toy", topic: "extension/toy",
burst_published_count: 0, burst_published_count: 0,
detected_nfc_ids: [],
last_error: null, last_error: null,
}; };
let peripheralCatalogRevision = null; let peripheralCatalogRevision = null;
@@ -4015,6 +4016,9 @@
active_extension_names: Array.isArray(value.active_extension_names) active_extension_names: Array.isArray(value.active_extension_names)
? value.active_extension_names.map((item) => String(item || "")).filter(Boolean) ? value.active_extension_names.map((item) => String(item || "")).filter(Boolean)
: [], : [],
detected_nfc_ids: Array.isArray(value.detected_nfc_ids)
? value.detected_nfc_ids.map(normalizeCardUid).filter(Boolean)
: [],
last_delivery_status: value.last_delivery_status || null, last_delivery_status: value.last_delivery_status || null,
last_error: value.last_error || null, last_error: value.last_error || null,
}; };
@@ -4035,11 +4039,14 @@
function applyNfcSnapshot(value, { announce = true } = {}) { function applyNfcSnapshot(value, { announce = true } = {}) {
if (!value || typeof value !== "object") return; if (!value || typeof value !== "object") return;
if (value.pet_cards) applyPetCardSnapshot(value.pet_cards); if (value.pet_cards) applyPetCardSnapshot(value.pet_cards);
if (value.extension_toy) { const extensionToyChanged = value.extension_toy
applyExtensionToySnapshot(value.extension_toy, { announce }); ? applyExtensionToySnapshot(value.extension_toy, { announce })
} : false;
const revision = Number(value.revision ?? -1); const revision = Number(value.revision ?? -1);
if (revision === nfcSnapshot.revision) return; if (revision === nfcSnapshot.revision) {
if (extensionToyChanged) renderUnifiedExtensionView?.();
return;
}
if (Array.isArray(value.legacy_profiles) && value.legacy_profiles.length) { if (Array.isArray(value.legacy_profiles) && value.legacy_profiles.length) {
importLegacyProfiles(value.legacy_profiles); importLegacyProfiles(value.legacy_profiles);
} }
@@ -4229,6 +4236,7 @@
reader: nfcSnapshot.reader, reader: nfcSnapshot.reader,
card: nfcSnapshot.card, card: nfcSnapshot.card,
}, },
detectedExtensionNfcIds: extensionToySnapshot.detected_nfc_ids,
pets: petCardSnapshot, pets: petCardSnapshot,
localDevices: state.devices.map((device) => ({ localDevices: state.devices.map((device) => ({
id: device.id, id: device.id,
@@ -4331,7 +4339,10 @@
item.enabled === false ? `${actionName}(已停用)` : actionName, item.enabled === false ? `${actionName}(已停用)` : actionName,
); );
}); });
const devices = [...grouped.values()]; const detectedNfcIds = new Set(extensionToySnapshot.detected_nfc_ids);
const devices = [...grouped.values()].filter(
(device) => detectedNfcIds.has(normalizeCardUid(device.cardUid)),
);
const lastAction = snapshot?.extension?.last_action; const lastAction = snapshot?.extension?.last_action;
const lastResult = snapshot?.extension?.result; const lastResult = snapshot?.extension?.result;
const nfcCapabilities = nfcDevice const nfcCapabilities = nfcDevice
@@ -4414,7 +4425,7 @@
<div>${device.actions.slice(0, 8).map((action) => `<span>${escapeHtml(action)}</span>`).join("")}</div> <div>${device.actions.slice(0, 8).map((action) => `<span>${escapeHtml(action)}</span>`).join("")}</div>
</article>`).join("") : nfcPet ? "" : ` </article>`).join("") : nfcPet ? "" : `
<div class="v2-extension-empty"> <div class="v2-extension-empty">
<i data-lucide="link-2-off"></i><div><strong>当前没有已绑定外设</strong><span>外设绑定 NFC 后将持续显示,解除绑定后移除</span></div> <i data-lucide="scan-line"></i><div><strong>等待 NFC 外设靠近</strong><span>检测到已绑定的 NFC 后,外设将在本次运行期间持续显示</span></div>
</div>`} </div>`}
</div> </div>
<footer class="v2-extension-linkage" data-active="${String(Boolean(visibleLastAction))}"> <footer class="v2-extension-linkage" data-active="${String(Boolean(visibleLastAction))}">
+1 -1
View File
@@ -12,7 +12,7 @@ const APP_SHELL = [
"/console-v2.css?v=delivery-state-69", "/console-v2.css?v=delivery-state-69",
"/console-v2.js?v=local-first-79-cinema-status", "/console-v2.js?v=local-first-79-cinema-status",
"/self-extension.css?v=pet-settings-54-dialog-size", "/self-extension.css?v=pet-settings-54-dialog-size",
"/self-extension.js?v=nfc-persistent-57", "/self-extension.js?v=nfc-session-58",
"/speech-input.css?v=sensevoice-7", "/speech-input.css?v=sensevoice-7",
"/speech-input.js?v=sensevoice-13", "/speech-input.js?v=sensevoice-13",
"/speech-audio-worklet.js?v=sensevoice-5", "/speech-audio-worklet.js?v=sensevoice-5",
@@ -167,6 +167,7 @@ class NfcExtensionToyPublisher:
"active_catalog_revision": None, "active_catalog_revision": None,
"active_extension_names": [], "active_extension_names": [],
"active_target_scopes": [], "active_target_scopes": [],
"detected_nfc_ids": [],
"message_count": 0, "message_count": 0,
"burst_frame_attempt_count": 0, "burst_frame_attempt_count": 0,
"burst_publish_attempt_count": 0, "burst_publish_attempt_count": 0,
@@ -192,6 +193,7 @@ class NfcExtensionToyPublisher:
active_catalog_revision=None, active_catalog_revision=None,
active_extension_names=[], active_extension_names=[],
active_target_scopes=[], active_target_scopes=[],
detected_nfc_ids=[],
message_count=0, message_count=0,
last_error=None, last_error=None,
) )
@@ -214,6 +216,7 @@ class NfcExtensionToyPublisher:
active_catalog_revision=None, active_catalog_revision=None,
active_extension_names=[], active_extension_names=[],
active_target_scopes=[], active_target_scopes=[],
detected_nfc_ids=[],
message_count=0, message_count=0,
) )
@@ -346,6 +349,14 @@ class NfcExtensionToyPublisher:
self._stop.wait(self.poll_interval_s) self._stop.wait(self.poll_interval_s)
continue continue
if card_uid:
with self._lock:
detected_nfc_ids = list(self._status["detected_nfc_ids"])
if card_uid not in detected_nfc_ids:
self._set_status(
detected_nfc_ids=[*detected_nfc_ids, card_uid],
)
try: try:
catalog_revision = self._catalog_revision() if card_uid else "" catalog_revision = self._catalog_revision() if card_uid else ""
except Exception as exc: # noqa: BLE001 - retry while the card remains except Exception as exc: # noqa: BLE001 - retry while the card remains
+2 -2
View File
@@ -1024,7 +1024,7 @@ operations:
) as response: ) as response:
speech_worklet = response.read().decode("utf-8") speech_worklet = response.read().decode("utf-8")
with self.opener.open( with self.opener.open(
f"{self.base_url}/self-extension.js?v=nfc-persistent-57", timeout=3 f"{self.base_url}/self-extension.js?v=nfc-session-58", timeout=3
) as response: ) as response:
extension_script = response.read().decode("utf-8") extension_script = response.read().decode("utf-8")
with self.opener.open( with self.opener.open(
@@ -1105,7 +1105,7 @@ operations:
self.assertIn('id="v2BootScreen"', template) self.assertIn('id="v2BootScreen"', template)
self.assertIn('正在加载车机控制与实时状态', template) self.assertIn('正在加载车机控制与实时状态', template)
self.assertIn( self.assertIn(
f'src="{runtime_url("self-extension.js", "/self-extension.js?v=nfc-persistent-57")}"', f'src="{runtime_url("self-extension.js", "/self-extension.js?v=nfc-session-58")}"',
html, html,
) )
self.assertIn('import "../self-extension.css";', vite_entry) self.assertIn('import "../self-extension.css";', vite_entry)
@@ -143,6 +143,7 @@ def test_each_detection_publishes_three_frames_per_device_and_then_stops() -> No
nfc["card"] = None nfc["card"] = None
assert wait_until(lambda: service.snapshot()["state"] == "waiting_for_nfc") assert wait_until(lambda: service.snapshot()["state"] == "waiting_for_nfc")
assert service.snapshot()["detected_nfc_ids"] == ["E113B203"]
nfc["card"] = {"action": "present", "uid": "E113B203"} nfc["card"] = {"action": "present", "uid": "E113B203"}
assert wait_until(lambda: len(publications) == 12) assert wait_until(lambda: len(publications) == 12)
assert service.snapshot()["state"] == "burst_complete" assert service.snapshot()["state"] == "burst_complete"
@@ -150,6 +151,29 @@ def test_each_detection_publishes_three_frames_per_device_and_then_stops() -> No
service.close() service.close()
def test_detected_devices_remain_latched_until_publisher_stops() -> None:
nfc: dict[str, Any] = {"card": {"action": "present", "uid": "E113B203"}}
service = NfcExtensionToyPublisher(
nfc_provider=lambda: nfc,
catalog_provider=lambda: [capability("light-on")],
publisher=lambda _payload: {"delivery_status": "queued"},
burst_window_s=0.03,
poll_interval_s=0.005,
)
service.start()
try:
assert wait_until(
lambda: service.snapshot()["detected_nfc_ids"] == ["E113B203"]
)
nfc["card"] = None
assert wait_until(lambda: service.snapshot()["state"] == "waiting_for_nfc")
assert service.snapshot()["detected_nfc_ids"] == ["E113B203"]
finally:
service.close()
assert service.snapshot()["detected_nfc_ids"] == []
def test_no_matching_ability_publishes_nothing() -> None: def test_no_matching_ability_publishes_nothing() -> None:
nfc = {"card": {"action": "present", "uid": "E113B203"}} nfc = {"card": {"action": "present", "uid": "E113B203"}}
publications: list[dict[str, Any]] = [] publications: list[dict[str, Any]] = []
@@ -168,6 +192,7 @@ def test_no_matching_ability_publishes_nothing() -> None:
assert wait_until( assert wait_until(
lambda: service.snapshot()["state"] == "no_matching_extension" lambda: service.snapshot()["state"] == "no_matching_extension"
) )
assert service.snapshot()["detected_nfc_ids"] == ["E113B203"]
time.sleep(0.05) time.sleep(0.05)
assert publications == [] assert publications == []
finally: finally:
@@ -94,7 +94,7 @@ def test_performance_assets_have_a_new_cache_generation() -> None:
for asset in ( for asset in (
"/app.js?v=dual-seat-147-video-media", "/app.js?v=dual-seat-147-video-media",
"/console-v2.js?v=local-first-79-cinema-status", "/console-v2.js?v=local-first-79-cinema-status",
"/self-extension.js?v=nfc-persistent-57", "/self-extension.js?v=nfc-session-58",
"/energy-monitor.js?v=energy-monitor-5-performance", "/energy-monitor.js?v=energy-monitor-5-performance",
): ):
assert asset in html assert asset in html
+3 -3
View File
@@ -65,15 +65,15 @@ def test_unified_console_exposes_device_id_based_nfc_authorization() -> None:
assert 'async function hydratePeripheralCatalog()' in script assert 'async function hydratePeripheralCatalog()' in script
assert 'api("peripherals")' in script assert 'api("peripherals")' in script
assert 'signalSource: "backend_catalog"' in script assert 'signalSource: "backend_catalog"' in script
assert '当前没有已绑定外设' in script assert '等待 NFC 外设靠近' in script
assert '外设绑定 NFC 后将持续显示,解除绑定后移除' in script assert '检测到已绑定的 NFC 后,外设将在本次运行期间持续显示' in script
assert '.v2-nfc-inline' in styles assert '.v2-nfc-inline' in styles
assert '.v2-nfc-dialog' in styles assert '.v2-nfc-dialog' in styles
assert '.v2-nfc-command-list' in styles assert '.v2-nfc-command-list' in styles
assert '.v2-nfc-command-option' in styles assert '.v2-nfc-command-option' in styles
assert '.v2-nfc-select-all' in styles assert '.v2-nfc-select-all' in styles
assert 'self-extension.js?v=nfc-persistent-57' in html assert 'self-extension.js?v=nfc-session-58' in html
assert 'import "../self-extension.css";' in vite_entry assert 'import "../self-extension.css";' in vite_entry
@@ -102,3 +102,17 @@ def test_device_card_surfaces_extension_toy_mqtt_delivery() -> None:
assert "extensionToySnapshot.last_error" in apply_toy assert "extensionToySnapshot.last_error" in apply_toy
assert "applyExtensionToySnapshot(value.extension_toy" in apply_nfc assert "applyExtensionToySnapshot(value.extension_toy" in apply_nfc
assert "正在准备 extension/toy 消息" in apply_nfc assert "正在准备 extension/toy 消息" in apply_nfc
def test_homepage_only_reveals_nfc_devices_detected_in_this_run() -> None:
source = SOURCE_PATH.read_text(encoding="utf-8")
unified = _function_source(
source,
" function buildUnifiedExtensionSync() {",
"\n function init()",
)
assert "detected_nfc_ids" in source
assert "const detectedNfcIds = new Set(" in unified
assert "detectedNfcIds.has(normalizeCardUid(device.cardUid))" in unified
assert "检测到已绑定的 NFC 后,外设将在本次运行期间持续显示" in unified