add mqtt and device test
This commit is contained in:
+27
@@ -0,0 +1,27 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEowIBAAKCAQEAskd8DRSgZIF9IqD/bEU8YMhWiLiyoP1Yl+MpFxxdyIJnIqXe
|
||||
6PRAgaZVPiy1vzYbr+acVBp4ZueBsVR5Tmd+u9009fzMi/VZkc2FBXrpbkBkyBE9
|
||||
ftiYiK7BcKJEQ2YqiFEIpGg6Jn009dvTyK35GLyfNM34XKzkhOjNElgcRpCTbFh5
|
||||
QhHrm6Wt3uv6Txb6rnoO+mroox6Pe/jd4XEh6vcwNOq324dCuTEeazv3OUkFnRzl
|
||||
7+WC55mzDU0ZevLkxQqGJYKnjUsmW4f2YmDGylRbbgVv4q6ycfg8VUyn2HiCJjJk
|
||||
4Q02KiVRmiYlZBJug2P7ZIyhrTNSOK2lit/ACwIDAQABAoIBABim04D4NAY3H5IN
|
||||
GB9PuD3GcKzJeVh937pqR1wc6IeZfIsTy5iDxHMJCIAnQjRqq3ZtxCUfCedqJgqw
|
||||
HHFaf0bum2xTpLHKXzR5lOYWl7ZOl4D7XU5abfEh1Q7EPqiL3+AR8HN8vrm5a3wp
|
||||
sFsaBLfxG8Sl9yyoHZXr87C7LUxrupOOB0gyqU2sdufA26yMEJXao/oes2DyxFLx
|
||||
P9k4tRdcZDfUF5jftkyQNvZ/XIZ/yavQ26GyHtts8hm9buBl/m83K44MjlHEIFv6
|
||||
KpkxLMICzxCNxsu13XKqQOpesgpOH7UNmS/jd9aqQKdRNj+wJIy29Y55aqYPzuFQ
|
||||
Lbp1Kl0CgYEAtbclUeP1WSJwXCFV7330Mzj1VxlOa61zC1imprSco+JGoSrRhpGo
|
||||
xiDvI9ZAh5M0jmfkDzQjbtGwj2F5UujxXeyKeSJngiQlDhMMgr2SfdKkDxm0Tmx+
|
||||
WpZnXvUBINIg+8OsgUA1Z/TM57ud3F/B657PAVNq07wIMRUlvmjG4s0CgYEA+yi8
|
||||
ju/1r9t/uJcVkjWLxzFnJ8qC8swmF3djmEJpM7Z4dUUVaMMB7hQfBQJmxM0DRWkt
|
||||
KMD47+gFQsOEn1w/GJxn/5GVQeyNUasvQt2sYe9HJIVCNbxSmIFhA0m3Sqy/z8fv
|
||||
InbtEOy/K7zVsPG3I5kNbJsLxDrIXxmpKRnVHjcCgYB+uWKFYXxQ1PuWxIixpB0R
|
||||
O7+dJkDSRvvcBc7yozI5+CtZagsE1b/lrEIZs+j8o2Qbi8g38hxjxEhlNYzujRUG
|
||||
c1d+csfMsnhFAHPRGXN329Yd0cc0ieT0N7+PMT3ALcpiyWscGDMmdEoRsX29meoa
|
||||
731dZ1cwogj0cdMInvlUYQKBgQDonFTuaS15nugOdNdEn5UCei3Yu4VGC28oAqna
|
||||
BX/bph6wNbhbW2h5MGd+QzgdAucJrRxnBzpHLvNYXy6ATXYefBURrKq48LX9snbG
|
||||
Dfouhea02zpz/CPfHMxVuDsqzQ2lCb3fhJeROkLf5jdfdq6wKHs3X+2o4uxar7Bs
|
||||
4YDxRwKBgAmqv2YkmxG9zP9keB1AWYiIZxiH7KiNy6Jl9jrdcuLvlTr5UzodkcUX
|
||||
8uIK77PWjiJ5SuXry4KYxb4QU1spc7xKUiKMhzzHx3IQOZco+isLrxxRldK5ZZu0
|
||||
H9WhfMh67rfyJ0kxtK6PXeBtAPcsoD3I2oblDdZg0RbXsS+VK8Rh
|
||||
-----END RSA PRIVATE KEY-----
|
||||
@@ -4,6 +4,7 @@ from api.roles import router as roles_router
|
||||
from api.auth import router as auth_router
|
||||
from api.device_control import router as device_control_router
|
||||
from api.ota import router as ota_router # 新增OTA路由
|
||||
from api.mqtt_router import router as mqtt_router # 新增MQTT路由
|
||||
|
||||
api_router = APIRouter()
|
||||
api_router.include_router(websocket_router, tags=["WebSocket"])
|
||||
@@ -11,4 +12,5 @@ api_router.include_router(roles_router, tags=["Roles"])
|
||||
api_router.include_router(auth_router, tags=["Auth"])
|
||||
api_router.include_router(device_control_router, tags=["Device Control"])
|
||||
api_router.include_router(ota_router, tags=["OTA"]) # 注册OTA路由
|
||||
api_router.include_router(mqtt_router, tags=["MQTT"]) # 注册MQTT路由
|
||||
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
from typing import Optional
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class GPSQueryRequest(BaseModel):
|
||||
device_id: str = Field(..., description="设备ID,如 TalkingQ_001")
|
||||
|
||||
|
||||
class VolumeRequest(BaseModel):
|
||||
device_id: str = Field(..., description="设备ID")
|
||||
level: int = Field(..., ge=0, le=100, description="音量等级 0-100")
|
||||
|
||||
|
||||
class OTARequest(BaseModel):
|
||||
device_id: str = Field(..., description="设备ID")
|
||||
url: str = Field(..., description="固件下载地址")
|
||||
version: str = Field(..., description="目标版本号")
|
||||
|
||||
|
||||
class NFCNoticeRequest(BaseModel):
|
||||
device_id: str = Field(..., description="设备ID")
|
||||
url: str = Field(..., description="提示音地址")
|
||||
|
||||
|
||||
class NFCListenRequest(BaseModel):
|
||||
device_id: str = Field(..., description="设备ID")
|
||||
uuid: str = Field(..., description="NFC卡片UUID")
|
||||
|
||||
|
||||
class NFCUnreadRequest(BaseModel):
|
||||
device_id: str = Field(..., description="设备ID")
|
||||
url: str = Field(..., description="未读留言音频地址")
|
||||
|
||||
|
||||
class NFCBindRequest(BaseModel):
|
||||
device_id: str = Field(..., description="设备ID")
|
||||
uuid: str = Field(..., description="NFC卡片UUID")
|
||||
|
||||
|
||||
class CommandResponse(BaseModel):
|
||||
code: int = 0
|
||||
message: str = "success"
|
||||
msg_id: str = Field(..., description="消息ID")
|
||||
device_id: str = Field(..., description="设备ID")
|
||||
|
||||
|
||||
class DeviceResponseData(BaseModel):
|
||||
msg_id: str
|
||||
status: Optional[str] = None
|
||||
data: Optional[dict] = None
|
||||
timestamp: Optional[float] = None
|
||||
|
||||
|
||||
class DeviceStatusResponse(BaseModel):
|
||||
device_id: str
|
||||
online: bool
|
||||
last_seen: Optional[float] = None
|
||||
@@ -0,0 +1,126 @@
|
||||
import logging
|
||||
from typing import Optional
|
||||
from fastapi import APIRouter, HTTPException, Query
|
||||
from services.offline_audio_cache import offline_audio_cache
|
||||
from api.mqtt_models import (
|
||||
GPSQueryRequest,
|
||||
VolumeRequest,
|
||||
OTARequest,
|
||||
NFCNoticeRequest,
|
||||
NFCListenRequest,
|
||||
NFCUnreadRequest,
|
||||
NFCBindRequest,
|
||||
CommandResponse,
|
||||
DeviceResponseData,
|
||||
DeviceStatusResponse,
|
||||
)
|
||||
from handlers.mqtt_handler import TalkingQMQTTService
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/api/mqtt", tags=["MQTT 服务"])
|
||||
|
||||
|
||||
async def _get_service() -> TalkingQMQTTService:
|
||||
service = await TalkingQMQTTService.get_instance()
|
||||
if service is None:
|
||||
raise HTTPException(status_code=503, detail="MQTT 服务未初始化")
|
||||
return service
|
||||
|
||||
|
||||
@router.post("/gps", response_model=CommandResponse, summary="GPS位置查询")
|
||||
async def query_gps(req: GPSQueryRequest):
|
||||
service = await _get_service()
|
||||
msg_id = await service.send_gps_query(req.device_id)
|
||||
return CommandResponse(msg_id=msg_id, device_id=req.device_id)
|
||||
|
||||
|
||||
@router.post("/volume", response_model=CommandResponse, summary="音量调节")
|
||||
async def set_volume(req: VolumeRequest):
|
||||
service = await _get_service()
|
||||
msg_id = await service.send_volume_command(req.device_id, req.level)
|
||||
return CommandResponse(msg_id=msg_id, device_id=req.device_id)
|
||||
|
||||
|
||||
@router.post("/ota", response_model=CommandResponse, summary="OTA升级")
|
||||
async def start_ota(req: OTARequest):
|
||||
service = await _get_service()
|
||||
msg_id = await service.send_ota_command(req.device_id, req.url, req.version)
|
||||
return CommandResponse(msg_id=msg_id, device_id=req.device_id)
|
||||
|
||||
|
||||
@router.post("/nfc/notice", response_model=CommandResponse, summary="NFC留言下发提示")
|
||||
async def send_nfc_notice(req: NFCNoticeRequest):
|
||||
service = await _get_service()
|
||||
msg_id = await service.send_nfc_notice(req.device_id, req.url)
|
||||
return CommandResponse(msg_id=msg_id, device_id=req.device_id)
|
||||
|
||||
|
||||
@router.post("/nfc/listen", response_model=CommandResponse, summary="NFC收听留言")
|
||||
async def nfc_listen(req: NFCListenRequest):
|
||||
service = await _get_service()
|
||||
topic = f"device/{req.device_id}/event"
|
||||
payload = {
|
||||
"msg_id": "005",
|
||||
"params": {"uuid": req.uuid}
|
||||
}
|
||||
await service._publish(topic, payload)
|
||||
return CommandResponse(msg_id="005", device_id=req.device_id)
|
||||
|
||||
|
||||
@router.post("/nfc/unread", response_model=CommandResponse, summary="设置NFC未读留言")
|
||||
async def set_nfc_unread(req: NFCUnreadRequest):
|
||||
await offline_audio_cache.add_audio_url(req.device_id, req.url)
|
||||
return CommandResponse(msg_id="", device_id=req.device_id, message="已设置未读留言")
|
||||
|
||||
|
||||
@router.delete("/nfc/unread/{device_id}", response_model=CommandResponse, summary="清除NFC未读留言定时推送")
|
||||
async def clear_nfc_unread(device_id: str):
|
||||
await offline_audio_cache.clear_audio_urls(device_id)
|
||||
return CommandResponse(msg_id="", device_id=device_id, message="已清除未读留言")
|
||||
|
||||
|
||||
@router.post("/nfc/bind", response_model=CommandResponse, summary="NFC绑定卡片")
|
||||
async def bind_nfc(req: NFCBindRequest):
|
||||
service = await _get_service()
|
||||
msg_id = await service.send_bind_nfc_command(req.device_id, req.uuid)
|
||||
return CommandResponse(msg_id=msg_id, device_id=req.device_id)
|
||||
|
||||
|
||||
# @router.get("/nfc/unread", summary="查询所有未读留言设备列表")
|
||||
# async def get_nfc_unread_devices():
|
||||
# service = await _get_service()
|
||||
# devices = service.get_nfc_unread_devices()
|
||||
# return {"count": len(devices), "devices": devices}
|
||||
|
||||
|
||||
# @router.get("/device/{device_id}/status", response_model=DeviceStatusResponse, summary="查询设备在线状态")
|
||||
# async def get_device_status(device_id: str):
|
||||
# service = await _get_service()
|
||||
# online = service.is_device_online(device_id)
|
||||
# last_seen = service.get_device_last_seen(device_id)
|
||||
# return DeviceStatusResponse(device_id=device_id, online=online, last_seen=last_seen)
|
||||
|
||||
|
||||
# @router.get("/device/{device_id}/responses", summary="查询设备上报记录")
|
||||
# async def get_device_responses(
|
||||
# device_id: str,
|
||||
# msg_id: Optional[str] = Query(None, description="按msg_id过滤"),
|
||||
# limit: int = Query(20, ge=1, le=100, description="返回条数"),
|
||||
# ):
|
||||
# service = await _get_service()
|
||||
# responses = service.get_device_responses(device_id, msg_id=msg_id, limit=limit)
|
||||
# return {"device_id": device_id, "count": len(responses), "responses": responses}
|
||||
|
||||
|
||||
# @router.get("/device/{device_id}/wait_response", summary="等待设备响应(同步阻塞)")
|
||||
# async def wait_device_response(
|
||||
# device_id: str,
|
||||
# msg_id: str = Query(..., description="等待的消息ID"),
|
||||
# timeout: float = Query(10.0, ge=1.0, le=60.0, description="超时时间(秒)"),
|
||||
# ):
|
||||
# service = await _get_service()
|
||||
# response = service.wait_for_response(device_id, msg_id, timeout=timeout)
|
||||
# if response is None:
|
||||
# raise HTTPException(status_code=408, detail=f"等待设备 {device_id} 的 msg_id={msg_id} 响应超时")
|
||||
# return response
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -36,10 +36,10 @@ class Settings(BaseSettings):
|
||||
db_password: str
|
||||
db_name: str = "talkingq"
|
||||
db_echo: bool = False # 是否打印SQL语句
|
||||
|
||||
|
||||
# TalkingQ设备MQTT命令服务配置
|
||||
talkingq_mqtt_broker: str = "broker.emqx.io"
|
||||
talkingq_mqtt_port: int = 1883
|
||||
talkingq_mqtt_port: int = 1884
|
||||
talkingq_mqtt_username: str = ""
|
||||
talkingq_mqtt_password: str = ""
|
||||
talkingq_mqtt_device_prefix: str = "TalkingQ"
|
||||
|
||||
@@ -2,7 +2,7 @@ import os
|
||||
import uuid
|
||||
from config import settings
|
||||
from utils.logger import session_logger
|
||||
from utils.audio_denoiser import reduce_background_noise
|
||||
# from utils.audio_denoiser import reduce_background_noise
|
||||
|
||||
|
||||
async def save_audio_file(audio_data: bytes, device_id: str) -> str:
|
||||
|
||||
@@ -0,0 +1,378 @@
|
||||
import json
|
||||
import time
|
||||
import asyncio
|
||||
from typing import Optional, Dict, Callable, Awaitable
|
||||
from config import settings
|
||||
from services.card_service import card_service
|
||||
from services.offline_audio_cache import offline_audio_cache
|
||||
import aiomqtt
|
||||
|
||||
from services.device_target_cache import device_target_cache
|
||||
from utils.logger import session_logger as logger
|
||||
|
||||
|
||||
class TalkingQMQTTService:
|
||||
_instance = None
|
||||
_lock = asyncio.Lock()
|
||||
|
||||
def __init__(self, config: dict):
|
||||
self.broker = config.get("broker", "broker.emqx.io")
|
||||
self.port = config.get("port", 1883)
|
||||
self.username = config.get("username")
|
||||
self.password = config.get("password")
|
||||
self.device_prefix = config.get("device_prefix", "TalkingQ")
|
||||
self.qos = config.get("qos", 0)
|
||||
self.keepalive = config.get("keepalive", 60)
|
||||
self.nfc_notice_interval = config.get("nfc_notice_interval", 600)
|
||||
|
||||
self._client: Optional[aiomqtt.Client] = None
|
||||
self._connected = False
|
||||
self._connect_lock = asyncio.Lock()
|
||||
self._message_task: Optional[asyncio.Task] = None
|
||||
|
||||
self._msg_handlers: Dict[str, Callable[[str, dict], Awaitable[None]]] = {
|
||||
"000": self._handle_device_info,
|
||||
"001": self._handle_gps_response,
|
||||
"002": self._handle_volume_response,
|
||||
"003": self._handle_ota_response,
|
||||
"004": self._handle_nfc_notice_response,
|
||||
"005": self._handle_nfc_listen_report,
|
||||
"006": self._handle_bind_response,
|
||||
"007": self._handle_open_response,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
async def get_instance(cls, config: dict = None) -> Optional["TalkingQMQTTService"]:
|
||||
async with cls._lock:
|
||||
if cls._instance is None and config is not None:
|
||||
cls._instance = cls(config)
|
||||
return cls._instance
|
||||
|
||||
@classmethod
|
||||
async def reset_instance(cls):
|
||||
async with cls._lock:
|
||||
if cls._instance is not None:
|
||||
await cls._instance.disconnect()
|
||||
cls._instance = None
|
||||
|
||||
async def _message_loop(self):
|
||||
try:
|
||||
async for message in self._client.messages:
|
||||
try:
|
||||
topic = str(message.topic)
|
||||
parts = topic.split("/")
|
||||
if parts[0] == "device":
|
||||
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}")
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
except Exception as e:
|
||||
logger.error("", "", f"消息循环异常: {e}")
|
||||
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}")
|
||||
# else:
|
||||
# logger.warning(device_id, "", f"[设备信息] 设备 {device_id} 查询失败: {payload}")
|
||||
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")
|
||||
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}")
|
||||
else:
|
||||
logger.warning(device_id, "", f"[GPS] 设备 {device_id} 查询失败: {payload}")
|
||||
|
||||
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}")
|
||||
else:
|
||||
logger.warning(device_id, "", f"[音量] 设备 {device_id} 调节失败: {payload}")
|
||||
|
||||
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}")
|
||||
else:
|
||||
logger.warning(device_id, "", f"[OTA] 设备 {device_id} 升级异常: {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}")
|
||||
|
||||
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}")
|
||||
|
||||
nfc_uuid = params.get("uuid")
|
||||
logger.info(device_id, "", f"[NFC绑定] 设备 {device_id} 请求绑定卡片, UUID={nfc_uuid}")
|
||||
|
||||
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}")
|
||||
|
||||
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
|
||||
|
||||
is_owner = await card_service.check_card_ownership(nfc_uuid, device_id)
|
||||
if is_owner:
|
||||
has_pending = await offline_audio_cache.has_pending_audio(device_id)
|
||||
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 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
|
||||
}
|
||||
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,
|
||||
"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:
|
||||
if self._connected:
|
||||
return
|
||||
try:
|
||||
self._client = aiomqtt.Client(
|
||||
hostname=self.broker,
|
||||
port=self.port,
|
||||
username=self.username,
|
||||
password=self.password,
|
||||
identifier=f"talkingq_server_{int(time.time())}",
|
||||
keepalive=self.keepalive,
|
||||
)
|
||||
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:
|
||||
self._connected = False
|
||||
logger.error("", "", f"TalkingQ MQTT 连接失败: {e}")
|
||||
|
||||
async def disconnect(self):
|
||||
if self._message_task and not self._message_task.done():
|
||||
self._message_task.cancel()
|
||||
try:
|
||||
await self._message_task
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
self._message_task = None
|
||||
|
||||
if self._client is not None:
|
||||
try:
|
||||
await self._client.__aexit__(None, None, None)
|
||||
except Exception:
|
||||
pass
|
||||
self._client = None
|
||||
self._connected = False
|
||||
|
||||
async def _ensure_connected(self):
|
||||
if not self._connected:
|
||||
await self.connect()
|
||||
|
||||
async def _publish(self, topic: str, payload: dict):
|
||||
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}")
|
||||
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
return "007"
|
||||
@@ -30,19 +30,19 @@ async def websocket_endpoint(websocket: WebSocket):
|
||||
await websocket.send_text('{"status": "error", "message": "Not authenticated"}')
|
||||
return
|
||||
|
||||
# 检查设备是否有离线音频URL需要发送
|
||||
has_pending = await offline_audio_cache.has_pending_audio(device_id)
|
||||
if has_pending:
|
||||
audio_urls = await offline_audio_cache.get_audio_urls(device_id)
|
||||
for audio_url in audio_urls:
|
||||
try:
|
||||
await websocket.send_text(f"SOUND_URL:{audio_url}")
|
||||
session_logger.info(device_id, "offline", f"发送离线音频URL: {audio_url}")
|
||||
except Exception as e:
|
||||
session_logger.error(device_id, "offline", f"发送离线音频URL失败: {e}")
|
||||
# 清空已发送的离线音频URL
|
||||
await offline_audio_cache.clear_audio_urls(device_id)
|
||||
session_logger.info(device_id, "offline", f"已清空设备的离线音频URL缓存,共 {len(audio_urls)} 个")
|
||||
# # 检查设备是否有离线音频URL需要发送
|
||||
# has_pending = await offline_audio_cache.has_pending_audio(device_id)
|
||||
# if has_pending:
|
||||
# audio_urls = await offline_audio_cache.get_audio_urls(device_id)
|
||||
# for audio_url in audio_urls:
|
||||
# try:
|
||||
# await websocket.send_text(f"SOUND_URL:{audio_url}")
|
||||
# session_logger.info(device_id, "offline", f"发送离线音频URL: {audio_url}")
|
||||
# except Exception as e:
|
||||
# session_logger.error(device_id, "offline", f"发送离线音频URL失败: {e}")
|
||||
# # 清空已发送的离线音频URL
|
||||
# await offline_audio_cache.clear_audio_urls(device_id)
|
||||
# session_logger.info(device_id, "offline", f"已清空设备的离线音频URL缓存,共 {len(audio_urls)} 个")
|
||||
|
||||
await handle_websocket_messages(websocket, device_id)
|
||||
|
||||
|
||||
@@ -267,18 +267,36 @@ async def process_cached_audio(device_id: str, target_device_id: str):
|
||||
audio_url = f"http://{settings.server_host}:{settings.server_port}/{audio_path}"
|
||||
|
||||
# 发送URL给目标设备
|
||||
target_websocket = await connection_manager.get_connection(target_device_id)
|
||||
if target_websocket and target_websocket.client_state.name == "CONNECTED":
|
||||
await target_websocket.send_text("TTS_START")
|
||||
session_logger.info(device_id, "target", "已发送 TTS_START 给客户端")
|
||||
await target_websocket.send_text(f"NFC_SOUND_URL:{audio_url}")
|
||||
session_logger.info(device_id, "target", f"已发送音频URL给目标设备 {target_device_id}: {audio_url}")
|
||||
await target_websocket.send_text("TTS_END")
|
||||
session_logger.info(device_id, "target", "已发送 TTS_END 给客户端")
|
||||
# target_websocket = await connection_manager.get_connection(target_device_id)
|
||||
# if target_websocket and target_websocket.client_state.name == "CONNECTED":
|
||||
# await target_websocket.send_text("TTS_START")
|
||||
# session_logger.info(device_id, "target", "已发送 TTS_START 给客户端")
|
||||
# await target_websocket.send_text(f"NFC_SOUND_URL:{audio_url}")
|
||||
# session_logger.info(device_id, "target", f"已发送音频URL给目标设备 {target_device_id}: {audio_url}")
|
||||
# await target_websocket.send_text("TTS_END")
|
||||
# session_logger.info(device_id, "target", "已发送 TTS_END 给客户端")
|
||||
# else:
|
||||
|
||||
# # 目标设备不在线,保存到离线缓存
|
||||
await offline_audio_cache.add_audio_url(target_device_id, audio_url)
|
||||
session_logger.warning(device_id, "target", f"目标设备 {target_device_id} 不在线,保存音频URL到离线缓存")
|
||||
websocket = await connection_manager.get_connection(device_id)
|
||||
if websocket and websocket.client_state.name == "CONNECTED":
|
||||
success_audio_url = f"http://{settings.server_host}:{settings.server_port}/assets/audio/message_ok.mp3"
|
||||
await websocket.send_text(f"PROMPT_SOUND_URL:{success_audio_url}")
|
||||
session_logger.info(device_id, "device", f"留言已收到音频URL给设备 {device_id}")
|
||||
else:
|
||||
# 目标设备不在线,保存到离线缓存
|
||||
await offline_audio_cache.add_audio_url(target_device_id, audio_url)
|
||||
session_logger.warning(device_id, "target", f"目标设备 {target_device_id} 不在线,保存音频URL到离线缓存")
|
||||
session_logger.warning(device_id, "device", f"设备 {device_id} 不在线,发送留言已收到音频URL失败")
|
||||
# # # 目标设备不在线,保存到离线缓存
|
||||
# await offline_audio_cache.add_audio_url(target_device_id, audio_url)
|
||||
# session_logger.warning(device_id, "target", f"目标设备 {target_device_id} 不在线,保存音频URL到离线缓存")
|
||||
# websocket = await connection_manager.get_connection(device_id)
|
||||
# if websocket and websocket.client_state.name == "CONNECTED":
|
||||
# success_audio_url = f"http://{settings.server_host}:{settings.server_port}/assets/audio/success_zh.mp3"
|
||||
# await websocket.send_text(f"NFC_MESSAGE_SUCCESS_URL:{success_audio_url}")
|
||||
# session_logger.info(device_id, "device", f"已发送留言成功音频URL给设备 {device_id}")
|
||||
# else:
|
||||
# session_logger.warning(device_id, "device", f"设备 {device_id} 不在线,发送留言成功音频失败")
|
||||
except Exception as e:
|
||||
session_logger.error(device_id, "target", f"处理缓存音频时出错: {e}", exc_info=True)
|
||||
finally:
|
||||
|
||||
@@ -2,6 +2,7 @@ import asyncio
|
||||
import os
|
||||
from contextlib import asynccontextmanager
|
||||
from fastapi import FastAPI
|
||||
from services import offline_audio_cache
|
||||
from services.device_config import DeviceConfigManager
|
||||
from services.schedule_session_cleanup import background_cleanup_task
|
||||
from services.role_manager import role_manager
|
||||
@@ -14,6 +15,8 @@ from api.assets import configure_static_assets
|
||||
from database.init_db import init_db
|
||||
from database.connection import get_db_manager
|
||||
from services.firmware_scanner import firmware_scanner
|
||||
from handlers.mqtt_handler import TalkingQMQTTService
|
||||
from services.scheduler import TaskScheduler
|
||||
|
||||
device_config_manager = DeviceConfigManager()
|
||||
|
||||
@@ -29,6 +32,28 @@ async def lifespan(app: FastAPI):
|
||||
)
|
||||
init_directories()
|
||||
|
||||
scheduler = TaskScheduler.get_instance()
|
||||
scheduler.start()
|
||||
scheduler.add_interval_task(
|
||||
interval_seconds=settings.talkingq_mqtt_nfc_notice_interval
|
||||
)
|
||||
if settings.talkingq_mqtt_broker:
|
||||
mqtt_config = {
|
||||
"broker": settings.talkingq_mqtt_broker,
|
||||
"port": settings.talkingq_mqtt_port,
|
||||
"username": settings.talkingq_mqtt_username,
|
||||
"password": settings.talkingq_mqtt_password,
|
||||
"device_prefix": settings.talkingq_mqtt_device_prefix,
|
||||
"qos": settings.talkingq_mqtt_qos,
|
||||
"keepalive": settings.talkingq_mqtt_keepalive,
|
||||
"nfc_notice_interval": settings.talkingq_mqtt_nfc_notice_interval,
|
||||
}
|
||||
service = await TalkingQMQTTService.get_instance(mqtt_config)
|
||||
await service.connect()
|
||||
session_logger.system_info("startup", "TalkingQ MQTT 设备命令服务链接已启动")
|
||||
else:
|
||||
session_logger.system_info("startup", "未配置 TalkingQ_MQTT,MQTT 服务链接未启动")
|
||||
exit(1)
|
||||
try:
|
||||
session_logger.system_info("startup", "初始化数据库...")
|
||||
await init_db()
|
||||
@@ -93,6 +118,10 @@ async def lifespan(app: FastAPI):
|
||||
if is_main_process:
|
||||
session_logger.system_info("shutdown", f"关闭数据库连接时出错: {str(e)}")
|
||||
|
||||
TaskScheduler.reset_instance()
|
||||
await TalkingQMQTTService.reset_instance()
|
||||
session_logger.system_info("shutdown", "TalkingQ MQTT 服务链接已关闭")
|
||||
|
||||
if is_main_process:
|
||||
session_logger.system_info("shutdown", "应用关闭")
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
aiofiles==24.1.0
|
||||
aiohttp==3.11.13
|
||||
aiohttp
|
||||
fastapi==0.115.11
|
||||
pycld2==0.41
|
||||
# pycld2==0.41
|
||||
pydantic-settings==2.8.1
|
||||
python-multipart==0.0.20
|
||||
PyYAML==6.0.2
|
||||
@@ -17,5 +17,6 @@ numpy==1.26.4
|
||||
scipy==1.11.4
|
||||
librosa==0.10.1
|
||||
setuptools==69.5.1
|
||||
|
||||
paho-mqtt>=2.0.0
|
||||
pycld2
|
||||
aiomqtt>=2.0.0
|
||||
apscheduler>=3.10.0
|
||||
@@ -25,7 +25,10 @@ class OfflineAudioCache:
|
||||
async def has_pending_audio(self, device_id: str) -> bool:
|
||||
async with self.lock:
|
||||
return device_id in self.offline_audio and len(self.offline_audio[device_id]) > 0
|
||||
|
||||
|
||||
async def get_all_audio_cache(self) -> List[str]:
|
||||
async with self.lock:
|
||||
return self.offline_audio
|
||||
|
||||
# 单例实例
|
||||
offline_audio_cache = OfflineAudioCache()
|
||||
@@ -0,0 +1,69 @@
|
||||
import logging
|
||||
from typing import Optional, Dict
|
||||
|
||||
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
||||
from apscheduler.triggers.interval import IntervalTrigger
|
||||
from services.connection_manager import connection_manager
|
||||
from handlers.mqtt_handler import TalkingQMQTTService
|
||||
from services.offline_audio_cache import offline_audio_cache
|
||||
from config import settings
|
||||
from utils.logger import session_logger as logger
|
||||
|
||||
class TaskScheduler:
|
||||
_instance = None
|
||||
|
||||
def __init__(self):
|
||||
self._scheduler = AsyncIOScheduler(timezone="Asia/Shanghai")
|
||||
|
||||
@classmethod
|
||||
def get_instance(cls) -> "TaskScheduler":
|
||||
if cls._instance is None:
|
||||
cls._instance = cls()
|
||||
return cls._instance
|
||||
|
||||
@classmethod
|
||||
def reset_instance(cls):
|
||||
if cls._instance is not None:
|
||||
cls._instance.shutdown()
|
||||
cls._instance = None
|
||||
|
||||
def start(self):
|
||||
if not self._scheduler.running:
|
||||
self._scheduler.start()
|
||||
logger.system_info("", "定时任务调度器已启动")
|
||||
|
||||
def shutdown(self):
|
||||
if self._scheduler.running:
|
||||
self._scheduler.shutdown(wait=False)
|
||||
logger.system_info("", "定时任务调度器已关闭")
|
||||
|
||||
async def _execute_task(self):
|
||||
try:
|
||||
service = await TalkingQMQTTService.get_instance()
|
||||
audio_cache = await offline_audio_cache.get_all_audio_cache()
|
||||
if not audio_cache:
|
||||
logger.warning("", "", "无离线音频缓存,跳过本次执行")
|
||||
return
|
||||
for device_id, audio_urls in audio_cache.items():
|
||||
websocket = await connection_manager.get_connection(device_id)
|
||||
if websocket is not None:
|
||||
audio_url = f"http://{settings.server_host}:{settings.server_port}/assets/audio/new_message.mp3"
|
||||
await service.send_nfc_notice(device_id, audio_url)
|
||||
logger.info(device_id, "", f"[定时任务] 发送音频成功: device={device_id}, audio_url={audio_url}")
|
||||
else:
|
||||
logger.info(device_id, "", f"[定时任务] 跳过音频发送: 设备 {device_id} 离线")
|
||||
except Exception as e:
|
||||
logger.error("", "", f"[定时任务] 执行失败: {e}")
|
||||
|
||||
def add_interval_task(self, interval_seconds: int = 600):
|
||||
task_id = "Voice_Message_Task"
|
||||
task_name = "Voice_Message_Task"
|
||||
job = self._scheduler.add_job(
|
||||
self._execute_task,
|
||||
trigger=IntervalTrigger(seconds=interval_seconds),
|
||||
id=task_id,
|
||||
name=task_name,
|
||||
replace_existing=True,
|
||||
)
|
||||
|
||||
logger.system_info("", f"[定时任务] 已添加: {task_id} | 间隔={interval_seconds}s | 下次执行={job.next_run_time}")
|
||||
Reference in New Issue
Block a user