feat(binding): support qr scan and nfc card binding flow
This commit is contained in:
@@ -1,16 +1,18 @@
|
||||
import asyncio
|
||||
import json
|
||||
import time
|
||||
import asyncio
|
||||
from typing import Optional, Dict, Callable, Awaitable
|
||||
from banban.service.device_setting import device_setting_service
|
||||
from config import settings
|
||||
from services.card_service import card_service
|
||||
from services.offline_audio_cache import offline_audio_cache
|
||||
import aiomqtt
|
||||
from banban.service.location import location_service
|
||||
from database.models import ChildLocationCurrent
|
||||
from typing import Awaitable, Callable, Dict, Optional
|
||||
|
||||
import aiomqtt
|
||||
|
||||
from banban.service.binding import BindingService
|
||||
from banban.service.device_setting import device_setting_service
|
||||
from banban.service.location import location_service
|
||||
from config import settings
|
||||
from database.models import ChildLocationCurrent
|
||||
from services.card_service import card_service
|
||||
from services.device_target_cache import device_target_cache
|
||||
from services.offline_audio_cache import offline_audio_cache
|
||||
from utils.logger import session_logger as logger
|
||||
|
||||
|
||||
@@ -64,130 +66,117 @@ class TalkingQMQTTService:
|
||||
try:
|
||||
topic = str(message.topic)
|
||||
parts = topic.split("/")
|
||||
if parts[0] == "device":
|
||||
device_id = parts[1] if len(parts) >= 2 else "unknown"
|
||||
if not parts or parts[0] != "device":
|
||||
continue
|
||||
|
||||
if not device_id.startswith(f"{self.device_prefix}_"):
|
||||
continue
|
||||
device_id = parts[1] if len(parts) >= 2 else "unknown"
|
||||
if not device_id.startswith(f"{self.device_prefix}_"):
|
||||
continue
|
||||
|
||||
payload = json.loads(message.payload.decode("utf-8"))
|
||||
|
||||
msg_id = payload.get("msg_id")
|
||||
|
||||
handler = self._msg_handlers.get(msg_id)
|
||||
if handler:
|
||||
await handler(device_id, payload)
|
||||
else:
|
||||
logger.warning(device_id, "", f"未知 msg_id: {msg_id}, 设备: {device_id}")
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error("", "", f"消息解析失败: {e}")
|
||||
except Exception as e:
|
||||
logger.error("", "", f"消息处理异常: {e}")
|
||||
payload = json.loads(message.payload.decode("utf-8"))
|
||||
msg_id = payload.get("msg_id")
|
||||
handler = self._msg_handlers.get(msg_id)
|
||||
if handler:
|
||||
await handler(device_id, payload)
|
||||
else:
|
||||
logger.warning(device_id, "", f"unknown msg_id={msg_id}")
|
||||
except json.JSONDecodeError as exc:
|
||||
logger.error("", "", f"invalid mqtt payload: {exc}")
|
||||
except Exception as exc:
|
||||
logger.error("", "", f"mqtt message handling failed: {exc}")
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
except Exception as e:
|
||||
logger.error("", "", f"消息循环异常: {e}")
|
||||
except Exception as exc:
|
||||
logger.error("", "", f"mqtt loop failed: {exc}")
|
||||
self._connected = False
|
||||
|
||||
async def _handle_device_info(self, device_id: str, payload: dict):
|
||||
# status = payload.get("status")
|
||||
data = payload.get("data", {})
|
||||
# if status == "success":
|
||||
d_id = data.get("id")
|
||||
power = data.get("power")
|
||||
signal = data.get("signal")
|
||||
version = data.get("version")
|
||||
voice = data.get("voice")
|
||||
logger.info(device_id, "", f"[设备信息] 设备 {d_id} 信息: 电量={power}, 信号强度={signal}, 版本号={version}, 音量={voice}")
|
||||
# 插入到数据库
|
||||
await device_setting_service.insert_or_update(device_id=device_id, power=power, signal_strength=signal, version_str=version, volume=voice)
|
||||
# else:
|
||||
# logger.warning(device_id, "", f"[设备信息] 设备 {device_id} 查询失败: {payload}")
|
||||
await device_setting_service.insert_or_update(
|
||||
device_id=device_id,
|
||||
power=data.get("power"),
|
||||
signal_strength=data.get("signal"),
|
||||
version_str=data.get("version"),
|
||||
volume=data.get("voice"),
|
||||
)
|
||||
await self._publish(f"device/{device_id}/event_resp", {"msg_id": "000", "status": "success"})
|
||||
|
||||
async def _handle_gps_response(self, device_id: str, payload: dict):
|
||||
status = payload.get("status")
|
||||
if payload.get("status") != "success":
|
||||
logger.warning(device_id, "", f"[GPS] query failed: {payload}")
|
||||
return
|
||||
|
||||
data = payload.get("data", {})
|
||||
if status == "success":
|
||||
lat = data.get("latitude")
|
||||
lon = data.get("longitude")
|
||||
logger.info(device_id, "", f"[GPS] 设备 {device_id} 位置: 纬度={lat}, 经度={lon}")
|
||||
# 插入到数据库
|
||||
location = ChildLocationCurrent(device_id=device_id, lat=lat, lon=lon)
|
||||
await location_service.insert_or_update(device_id=device_id, location=location)
|
||||
else:
|
||||
logger.warning(device_id, "", f"[GPS] 设备 {device_id} 查询失败: {payload}")
|
||||
location = ChildLocationCurrent(
|
||||
device_id=device_id,
|
||||
lat=data.get("latitude"),
|
||||
lon=data.get("longitude"),
|
||||
)
|
||||
await location_service.insert_or_update(device_id=device_id, location=location)
|
||||
|
||||
async def _handle_volume_response(self, device_id: str, payload: dict):
|
||||
status = payload.get("status")
|
||||
data = payload.get("data", {})
|
||||
if status == "success":
|
||||
level = data.get("current_level")
|
||||
logger.info(device_id, "", f"[音量] 设备 {device_id} 当前音量: {level}")
|
||||
# 更新设备音量
|
||||
await device_setting_service.insert_or_update(device_id=device_id, volume=level)
|
||||
if payload.get("status") != "success":
|
||||
logger.warning(device_id, "", f"[volume] command failed: {payload}")
|
||||
return
|
||||
|
||||
else:
|
||||
logger.warning(device_id, "", f"[音量] 设备 {device_id} 调节失败: {payload}")
|
||||
data = payload.get("data", {})
|
||||
await device_setting_service.insert_or_update(device_id=device_id, volume=data.get("current_level"))
|
||||
|
||||
async def _handle_ota_response(self, device_id: str, payload: dict):
|
||||
status = payload.get("status")
|
||||
data = payload.get("data", {})
|
||||
if status == "accepted":
|
||||
current = data.get("current_version")
|
||||
target = data.get("target_version")
|
||||
logger.info(device_id, "", f"[OTA] 设备 {device_id} 已接受升级: {current} -> {target}")
|
||||
elif status == "success":
|
||||
new_ver = data.get("new_version")
|
||||
logger.info(device_id, "", f"[OTA] 设备 {device_id} 升级完成: {new_ver}")
|
||||
# 更新设备版本号
|
||||
await device_setting_service.insert_or_update(device_id=device_id, version_str=new_ver)
|
||||
else:
|
||||
logger.warning(device_id, "", f"[OTA] 设备 {device_id} 升级异常: {payload}")
|
||||
if status == "success":
|
||||
await device_setting_service.insert_or_update(device_id=device_id, version_str=data.get("new_version"))
|
||||
elif status != "accepted":
|
||||
logger.warning(device_id, "", f"[OTA] command failed: {payload}")
|
||||
|
||||
async def _handle_nfc_notice_response(self, device_id: str, payload: dict):
|
||||
status = payload.get("status")
|
||||
if status == "success":
|
||||
logger.info(device_id, "", f"[NFC通知] 设备 {device_id} 已收到留言提示")
|
||||
else:
|
||||
logger.warning(device_id, "", f"[NFC通知] 设备 {device_id} 留言提示失败: {payload}")
|
||||
if status != "success":
|
||||
logger.warning(device_id, "", f"[NFC notice] command failed: {payload}")
|
||||
|
||||
async def _handle_nfc_listen_report(self, device_id: str, payload: dict):
|
||||
params = payload.get("params", {})
|
||||
nfc_uuid = params.get("uuid")
|
||||
logger.info(device_id, "", f"[NFC收听] 设备 {device_id} 请求收听留言, UUID={nfc_uuid}")
|
||||
await self._send_nfc_listen_response(device_id, nfc_uuid)
|
||||
|
||||
|
||||
async def _handle_bind_response(self, device_id: str, payload: dict):
|
||||
params = payload.get("params", {})
|
||||
status = params.get("status")
|
||||
logger.info(device_id, "", f"[NFC绑定] 设备 {device_id} 请求绑定卡片返回状态: {status}")
|
||||
data = payload.get("data", {})
|
||||
status = payload.get("status") or params.get("status")
|
||||
logger.info(device_id, "", f"[NFC bind] device={device_id} status={status}")
|
||||
|
||||
nfc_uuid = params.get("uuid")
|
||||
# 卡片与设备绑定
|
||||
await card_service.activate_card(device_id=device_id, card_uuid=nfc_uuid)
|
||||
logger.info(device_id, "", f"[NFC绑定] 设备 {device_id} 请求绑定卡片, UUID={nfc_uuid}")
|
||||
if status not in (None, "success", "accepted"):
|
||||
logger.warning(device_id, "", f"[NFC bind] bind failed: {payload}")
|
||||
return
|
||||
|
||||
nfc_uuid = data.get("uuid") or params.get("uuid")
|
||||
if not nfc_uuid:
|
||||
logger.warning(device_id, "", f"[NFC bind] missing uuid: {payload}")
|
||||
return
|
||||
|
||||
service = BindingService()
|
||||
result = await service.finalize_nfc_bind(device_id=device_id, card_uuid=nfc_uuid)
|
||||
if result is None:
|
||||
logger.warning(device_id, "", "[NFC bind] no pending bind session found")
|
||||
return
|
||||
|
||||
logger.info(device_id, "", f"[NFC bind] bind completed, uuid={nfc_uuid}, result={result}")
|
||||
|
||||
async def _handle_open_response(self, device_id: str, payload: dict):
|
||||
params = payload.get("params", {})
|
||||
status = params.get("status")
|
||||
logger.info(device_id, "", f"[设备状态] 设备 {device_id} 请求返回设备状态: {status}")
|
||||
data = params.get("data", {})
|
||||
device_status = data.get("status")
|
||||
logger.info(device_id, "", f"[设备状态] 设备 {device_id} 请求设备状态: {device_status}")
|
||||
logger.info(device_id, "", f"[device open] response={params}")
|
||||
|
||||
async def _send_nfc_listen_response(self, device_id: str, nfc_uuid: str):
|
||||
topic = f"device/{device_id}/event_resp"
|
||||
card = await card_service.get_card_by_uuid(nfc_uuid)
|
||||
if not card:
|
||||
logger.warning("", "", f"[NFC收听] 设备 {device_id} 没有找到卡片{nfc_uuid},无法发送留言提示")
|
||||
payload = {
|
||||
"msg_id": "005",
|
||||
"type": 0,
|
||||
"params": {
|
||||
"url_1": f"http://{settings.server_host}:{settings.server_port}/assets/audio/error_card.mp3"
|
||||
}
|
||||
},
|
||||
}
|
||||
await self._publish(topic, payload)
|
||||
return
|
||||
@@ -201,75 +190,54 @@ class TalkingQMQTTService:
|
||||
"type": 0,
|
||||
"params": {
|
||||
"url_1": f"http://{settings.server_host}:{settings.server_port}/assets/audio/test_zh.mp3"
|
||||
}
|
||||
},
|
||||
}
|
||||
await self._publish(topic, payload)
|
||||
return
|
||||
return
|
||||
|
||||
if has_pending:
|
||||
audio_urls = await offline_audio_cache.get_audio_urls(device_id)
|
||||
# 53D92B6DA20001 测试卡片
|
||||
if nfc_uuid == "53D92B6DA20001":
|
||||
payload = {
|
||||
"msg_id": "005",
|
||||
"type": 0,
|
||||
"params": {
|
||||
"url_1": f"http://{settings.server_host}:{settings.server_port}/assets/audio/test_zh.mp3"
|
||||
}
|
||||
}
|
||||
await self._publish(topic, payload)
|
||||
return
|
||||
if len(audio_urls) == 0:
|
||||
payload = {
|
||||
"msg_id": "005",
|
||||
"type": 0,
|
||||
"params": {
|
||||
"url_1": f"http://{settings.server_host}:{settings.server_port}/assets/audio/no_message.mp3"
|
||||
}
|
||||
},
|
||||
}
|
||||
else:
|
||||
params = {}
|
||||
for k, audio_url in enumerate(audio_urls, start=1):
|
||||
params[f"url_{k}"] = audio_url
|
||||
payload = {
|
||||
"msg_id": "005",
|
||||
"type": 0,
|
||||
"params": params
|
||||
}
|
||||
params = {f"url_{k}": audio_url for k, audio_url in enumerate(audio_urls, start=1)}
|
||||
payload = {"msg_id": "005", "type": 0, "params": params}
|
||||
await offline_audio_cache.clear_audio_urls(device_id)
|
||||
await self._publish(topic, payload)
|
||||
else:
|
||||
payload = {
|
||||
"msg_id": "005",
|
||||
"type": 0,
|
||||
"params": {
|
||||
"url_1": f"http://{settings.server_host}:{settings.server_port}/assets/audio/no_message.mp3"
|
||||
}
|
||||
}
|
||||
await self._publish(topic, payload)
|
||||
else:
|
||||
# 检查卡片是否存在
|
||||
existing_card = await card_service.get_card_by_uuid(nfc_uuid)
|
||||
|
||||
if existing_card:
|
||||
# 卡片已存在,使用卡片绑定的设备ID作为目标设备ID
|
||||
target_device_id = existing_card.device_id
|
||||
logger.info(device_id, "card", f"卡片已存在,绑定的设备ID: {target_device_id}")
|
||||
else:
|
||||
# 卡片不存在,创建新卡片并绑定到当前设备
|
||||
new_card = await card_service.activate_card(nfc_uuid, device_id)
|
||||
logger.info(device_id, "card", f"新卡片{nfc_uuid}已创建并激活,绑定到设备: {device_id}")
|
||||
return
|
||||
|
||||
# 设置目标设备
|
||||
await device_target_cache.set_target(device_id, target_device_id)
|
||||
|
||||
payload = {
|
||||
"msg_id": "005",
|
||||
"type": 1,
|
||||
"type": 0,
|
||||
"params": {
|
||||
"url_1": f"http://{settings.server_host}:{settings.server_port}/assets/audio/welcome.mp3"
|
||||
}
|
||||
"url_1": f"http://{settings.server_host}:{settings.server_port}/assets/audio/no_message.mp3"
|
||||
},
|
||||
}
|
||||
await self._publish(topic, payload)
|
||||
return
|
||||
|
||||
existing_card = await card_service.get_card_by_uuid(nfc_uuid)
|
||||
if existing_card:
|
||||
target_device_id = existing_card.device_id
|
||||
else:
|
||||
await card_service.activate_card(nfc_uuid, device_id)
|
||||
return
|
||||
|
||||
await device_target_cache.set_target(device_id, target_device_id)
|
||||
payload = {
|
||||
"msg_id": "005",
|
||||
"type": 1,
|
||||
"params": {
|
||||
"url_1": f"http://{settings.server_host}:{settings.server_port}/assets/audio/welcome.mp3"
|
||||
},
|
||||
}
|
||||
await self._publish(topic, payload)
|
||||
|
||||
async def connect(self):
|
||||
async with self._connect_lock:
|
||||
@@ -286,19 +254,15 @@ class TalkingQMQTTService:
|
||||
)
|
||||
await self._client.__aenter__()
|
||||
self._connected = True
|
||||
logger.system_info("", f"TalkingQ MQTT 已连接: {self.broker}:{self.port}")
|
||||
|
||||
await self._client.subscribe("device/+/response", qos=self.qos)
|
||||
logger.system_info("", "已订阅: device/+/response")
|
||||
|
||||
await self._client.subscribe("device/+/event", qos=self.qos)
|
||||
logger.system_info("", "已订阅: device/+/event")
|
||||
|
||||
self._message_task = asyncio.create_task(self._message_loop())
|
||||
logger.info("", "", "TalkingQ MQTT 服务启动中...")
|
||||
except Exception as e:
|
||||
logger.info("", "", f"mqtt connected: {self.broker}:{self.port}")
|
||||
except Exception as exc:
|
||||
self._connected = False
|
||||
logger.error("", "", f"TalkingQ MQTT 连接失败: {e}")
|
||||
logger.error("", "", f"mqtt connect failed: {exc}")
|
||||
|
||||
async def disconnect(self):
|
||||
if self._message_task and not self._message_task.done():
|
||||
@@ -325,69 +289,48 @@ class TalkingQMQTTService:
|
||||
await self._ensure_connected()
|
||||
try:
|
||||
await self._client.publish(topic, json.dumps(payload, ensure_ascii=False), qos=self.qos)
|
||||
logger.info("", "", f"下发命令 -> {topic}: {payload}")
|
||||
except Exception as e:
|
||||
logger.error("", "", f"命令下发失败: {e}")
|
||||
logger.info("", "", f"mqtt publish topic={topic} payload={payload}")
|
||||
except Exception as exc:
|
||||
logger.error("", "", f"mqtt publish failed: {exc}")
|
||||
|
||||
async def send_gps_query(self, device_id: str) -> str:
|
||||
topic = f"device/{device_id}/command"
|
||||
payload = {"msg_id": "001"}
|
||||
await self._publish(topic, payload)
|
||||
await self._publish(f"device/{device_id}/command", {"msg_id": "001"})
|
||||
return "001"
|
||||
|
||||
async def send_volume_command(self, device_id: str, level: int) -> str:
|
||||
topic = f"device/{device_id}/command"
|
||||
payload = {
|
||||
"msg_id": "002",
|
||||
"params": {"level": level}
|
||||
}
|
||||
await self._publish(topic, payload)
|
||||
await self._publish(
|
||||
f"device/{device_id}/command",
|
||||
{"msg_id": "002", "params": {"level": level}},
|
||||
)
|
||||
return "002"
|
||||
|
||||
async def send_ota_command(self, device_id: str, url: str, version: str) -> str:
|
||||
topic = f"device/{device_id}/command"
|
||||
payload = {
|
||||
"msg_id": "003",
|
||||
"params": {
|
||||
"url": url,
|
||||
"version": version,
|
||||
}
|
||||
}
|
||||
await self._publish(topic, payload)
|
||||
await self._publish(
|
||||
f"device/{device_id}/command",
|
||||
{"msg_id": "003", "params": {"url": url, "version": version}},
|
||||
)
|
||||
return "003"
|
||||
|
||||
async def send_nfc_notice(self, device_id: str, url: str) -> str:
|
||||
topic = f"device/{device_id}/command"
|
||||
payload = {
|
||||
"msg_id": "004",
|
||||
"params": {"url_1": url}
|
||||
}
|
||||
await self._publish(topic, payload)
|
||||
await self._publish(
|
||||
f"device/{device_id}/command",
|
||||
{"msg_id": "004", "params": {"url_1": url}},
|
||||
)
|
||||
return "004"
|
||||
|
||||
|
||||
async def send_bind_nfc_command(self, device_id: str, uuid: str) -> str:
|
||||
topic = f"device/{device_id}/command"
|
||||
payload = {
|
||||
"msg_id": "006"
|
||||
}
|
||||
await self._publish(topic, payload)
|
||||
async def send_bind_nfc_command(self, device_id: str, uuid: Optional[str] = None) -> str:
|
||||
del uuid
|
||||
await self._publish(f"device/{device_id}/command", {"msg_id": "006"})
|
||||
return "006"
|
||||
|
||||
async def send_open_command(self, device_id: str, open_type: int, is_open: int) -> str:
|
||||
topic = f"device/{device_id}/command"
|
||||
if open_type not in [0, 1]:
|
||||
raise ValueError("open_type must be 0 or 1")
|
||||
|
||||
if open_type == 0:
|
||||
payload = {
|
||||
"msg_id": "007",
|
||||
"type": open_type
|
||||
}
|
||||
elif open_type == 1:
|
||||
payload = {
|
||||
"msg_id": "007",
|
||||
"type": open_type,
|
||||
"status": is_open
|
||||
}
|
||||
await self._publish(topic, payload)
|
||||
payload = {"msg_id": "007", "type": open_type}
|
||||
else:
|
||||
payload = {"msg_id": "007", "type": open_type, "status": is_open}
|
||||
|
||||
await self._publish(f"device/{device_id}/command", payload)
|
||||
return "007"
|
||||
|
||||
Reference in New Issue
Block a user