mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-28 10:03:54 +08:00
194 lines
7.4 KiB
Python
194 lines
7.4 KiB
Python
import asyncio
|
|
from aiohttp import web
|
|
from config.logger import setup_logging
|
|
from core.api.native_mqtt_management_handler import (
|
|
NativeMqttManagementHandler,
|
|
)
|
|
from core.api.ota_handler import OTAHandler
|
|
from core.api.vision_handler import VisionHandler
|
|
|
|
TAG = __name__
|
|
|
|
|
|
class SimpleHttpServer:
|
|
def __init__(self, config: dict, management_owner=None):
|
|
self.config = config
|
|
self.management_owner = management_owner
|
|
self.logger = setup_logging()
|
|
self.ota_handler = OTAHandler(config)
|
|
self.vision_handler = VisionHandler(config)
|
|
self.native_mqtt_management_handler = (
|
|
NativeMqttManagementHandler(config, management_owner)
|
|
if management_owner is not None and self._native_mqtt_enabled()
|
|
else None
|
|
)
|
|
self._started_event = asyncio.Event()
|
|
self._stop_event = asyncio.Event()
|
|
self._runner = None
|
|
self._start_active = False
|
|
self._cleanup_lock = asyncio.Lock()
|
|
|
|
def _native_mqtt_enabled(self) -> bool:
|
|
mqtt_config = self.config.get("mqtt_server", {})
|
|
enabled_protocols = self.config.get("enabled_protocols", [])
|
|
return (
|
|
isinstance(mqtt_config, dict)
|
|
and mqtt_config.get("enabled") is True
|
|
and "mqtt" in enabled_protocols
|
|
)
|
|
|
|
async def wait_started(self, task: asyncio.Task, timeout: float = 10) -> None:
|
|
"""Wait until the HTTP listener is bound or surface startup failure."""
|
|
event_waiter = asyncio.create_task(self._started_event.wait())
|
|
try:
|
|
done, _ = await asyncio.wait(
|
|
{task, event_waiter},
|
|
timeout=timeout,
|
|
return_when=asyncio.FIRST_COMPLETED,
|
|
)
|
|
if self._started_event.is_set():
|
|
if task.done():
|
|
await task
|
|
return
|
|
if task in done:
|
|
await task
|
|
raise RuntimeError("HTTP服务器在就绪前退出")
|
|
raise TimeoutError("等待HTTP服务器启动超时")
|
|
finally:
|
|
if not event_waiter.done():
|
|
event_waiter.cancel()
|
|
await asyncio.gather(event_waiter, return_exceptions=True)
|
|
|
|
def _get_websocket_url(self, local_ip: str, port: int) -> str:
|
|
"""获取websocket地址
|
|
|
|
Args:
|
|
local_ip: 本地IP地址
|
|
port: 端口号
|
|
|
|
Returns:
|
|
str: websocket地址
|
|
"""
|
|
server_config = self.config["server"]
|
|
websocket_config = server_config.get("websocket")
|
|
|
|
if websocket_config and "你" not in websocket_config:
|
|
return websocket_config
|
|
else:
|
|
return f"ws://{local_ip}:{port}/xiaozhi/v1/"
|
|
|
|
async def start(self):
|
|
runner = None
|
|
self._start_active = True
|
|
try:
|
|
self._started_event.clear()
|
|
self._stop_event.clear()
|
|
server_config = self.config["server"]
|
|
read_config_from_api = self.config.get("read_config_from_api", False)
|
|
host = server_config.get("ip", "0.0.0.0")
|
|
port = int(server_config.get("http_port", 8003))
|
|
|
|
if port:
|
|
mqtt_config = self.config.get("mqtt_server", {})
|
|
client_max_size = max(
|
|
1024,
|
|
int(
|
|
mqtt_config.get(
|
|
"manager_max_request_size", 64 * 1024
|
|
)
|
|
or 64 * 1024
|
|
),
|
|
)
|
|
app = web.Application(client_max_size=client_max_size)
|
|
|
|
if not read_config_from_api:
|
|
# 如果没有开启智控台,只是单模块运行,就需要再添加简单OTA接口,用于下发websocket接口
|
|
app.add_routes(
|
|
[
|
|
web.get("/xiaozhi/ota/", self.ota_handler.handle_get),
|
|
web.post("/xiaozhi/ota/", self.ota_handler.handle_post),
|
|
web.options(
|
|
"/xiaozhi/ota/", self.ota_handler.handle_options
|
|
),
|
|
# 下载接口,仅提供 data/bin/*.bin 下载
|
|
web.get(
|
|
"/xiaozhi/ota/download/{filename}",
|
|
self.ota_handler.handle_download,
|
|
),
|
|
web.options(
|
|
"/xiaozhi/ota/download/{filename}",
|
|
self.ota_handler.handle_options,
|
|
),
|
|
]
|
|
)
|
|
# 添加路由
|
|
app.add_routes(
|
|
[
|
|
web.get("/mcp/vision/explain", self.vision_handler.handle_get),
|
|
web.post(
|
|
"/mcp/vision/explain", self.vision_handler.handle_post
|
|
),
|
|
web.options(
|
|
"/mcp/vision/explain", self.vision_handler.handle_options
|
|
),
|
|
]
|
|
)
|
|
if self.native_mqtt_management_handler is not None:
|
|
app.add_routes(
|
|
[
|
|
web.post(
|
|
"/api/devices/status",
|
|
self.native_mqtt_management_handler.handle_device_status,
|
|
),
|
|
web.post(
|
|
"/api/commands/{client_id}",
|
|
self.native_mqtt_management_handler.handle_command,
|
|
),
|
|
web.post(
|
|
"/api/call/request",
|
|
self.native_mqtt_management_handler.handle_call_request,
|
|
),
|
|
web.post(
|
|
"/api/call/accept",
|
|
self.native_mqtt_management_handler.handle_call_accept,
|
|
),
|
|
]
|
|
)
|
|
|
|
# 运行服务
|
|
runner = web.AppRunner(app)
|
|
self._runner = runner
|
|
await runner.setup()
|
|
site = web.TCPSite(runner, host, port)
|
|
await site.start()
|
|
self._started_event.set()
|
|
await self._stop_event.wait()
|
|
except Exception as e:
|
|
self.logger.bind(tag=TAG).error(f"HTTP服务器启动失败: {e}")
|
|
import traceback
|
|
|
|
self.logger.bind(tag=TAG).error(f"错误堆栈: {traceback.format_exc()}")
|
|
raise
|
|
finally:
|
|
self._started_event.clear()
|
|
try:
|
|
if runner is not None:
|
|
await self._cleanup_runner()
|
|
finally:
|
|
self._start_active = False
|
|
|
|
async def _cleanup_runner(self):
|
|
"""Cleanup the owned runner and retain it when cleanup must be retried."""
|
|
async with self._cleanup_lock:
|
|
runner = self._runner
|
|
if runner is None:
|
|
return
|
|
await runner.cleanup()
|
|
self._runner = None
|
|
|
|
async def stop(self):
|
|
"""Stop the HTTP loop and retry cleanup left by a failed start task."""
|
|
self._stop_event.set()
|
|
if not self._start_active:
|
|
await self._cleanup_runner()
|