DroneMind/voice_drone/core/scoket_client.py
2026-04-14 09:54:26 +08:00

239 lines
8.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Socket客户端
"""
import socket
import json
import time
from voice_drone.core.configuration import SYSTEM_SOCKET_SERVER_CONFIG
from voice_drone.core.command import Command
from voice_drone.logging_ import get_logger
logger = get_logger("socket.client")
class SocketClient:
# 初始化Socket客户端
def __init__(self, config: dict):
self.host = config.get("host")
self.port = config.get("port")
self.connect_timeout = config.get("connect_timeout")
self.send_timeout = config.get("send_timeout")
self.reconnect_interval = float(config.get("reconnect_interval") or 3.0)
# max_retries-1 表示断线后持续重连并发送,直到成功(不视为致命错误)
_mr = config.get("max_retries", -1)
try:
self.max_reconnect_attempts = int(_mr)
except (TypeError, ValueError):
self.max_reconnect_attempts = -1
self.sock = None
self.connected = False
# 连接到socket服务器
def connect(self) -> bool:
if self.connected and self.sock is not None:
return True
try:
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.sock.settimeout(self.connect_timeout)
self.sock.connect((self.host, self.port))
self.sock.settimeout(self.send_timeout)
self.connected = True
print(
f"[SocketClient] 连接成功: {self.host}:{self.port}",
flush=True,
)
logger.info("Socket 已连接 %s:%s", self.host, self.port)
return True
except socket.timeout:
logger.warning(
"Socket 连接超时 host=%s port=%s timeout=%s",
self.host,
self.port,
self.connect_timeout,
)
print(
"[SocketClient] connect: 连接超时 "
f"(host={self.host!r}, port={self.port!r}, timeout={self.connect_timeout!r})",
flush=True,
)
self._cleanup()
return False
except ConnectionRefusedError as e:
logger.warning("Socket 连接被拒绝: %s", e)
print(
f"[SocketClient] connect: 连接被拒绝: {e!r}",
flush=True,
)
self._cleanup()
return False
except OSError as e:
logger.warning("Socket connect OSError (%s): %s", type(e).__name__, e)
print(
f"[SocketClient] connect: OSError ({type(e).__name__}): {e!r}",
flush=True,
)
self._cleanup()
return False
except Exception as e:
print(
f"[SocketClient] connect: 未预期异常 ({type(e).__name__}): {e!r}",
flush=True,
)
self._cleanup()
return False
# 断开与socket服务器的连接
def disconnect(self) -> None:
self._cleanup()
# 清理资源
def _cleanup(self) -> None:
if self.sock is not None:
try:
self.sock.close()
except Exception:
pass
self.sock = None
self.connected = False
# 确保连接已建立
def _ensure_connected(self) -> bool:
if self.connected and self.sock is not None:
return True
return self.connect()
# 发送命令
def send_command(self, command) -> bool:
print("[SocketClient] 正在发送命令…", flush=True)
if not self._ensure_connected():
logger.warning(
"Socket 未连接且 connect 失败,跳过本次发送 host=%s port=%s",
self.host,
self.port,
)
print(
"[SocketClient] 未连接或 connect 失败,跳过发送",
flush=True,
)
return False
try:
command_dict = command.to_dict()
json_str = json.dumps(command_dict, ensure_ascii=False)
# 添加换行符(根据 JSON格式说明.md命令以换行符分隔
message = json_str + "\n"
# 发送数据
self.sock.sendall(message.encode("utf-8"))
print("[SocketClient] sendall 成功", flush=True)
return True
except socket.timeout:
logger.warning("Socket send 超时,将断开以便重连")
print("[SocketClient] send_command: socket 超时", flush=True)
self._cleanup()
return False
except ConnectionResetError as e:
logger.warning("Socket 连接被重置(将重连): %s", e)
print(
f"[SocketClient] send_command: 连接被重置 ({type(e).__name__}): {e!r}",
flush=True,
)
self._cleanup()
return False
except BrokenPipeError as e:
logger.warning("Socket 管道破裂(将重连): %s", e)
print(
f"[SocketClient] send_command: 管道破裂 ({type(e).__name__}): {e!r}",
flush=True,
)
self._cleanup()
return False
except OSError as e:
# 断网、对端关闭等:可恢复,不当作未捕获致命错误
logger.warning("Socket send OSError (%s): %s(将重连)", type(e).__name__, e)
print(
f"[SocketClient] send_command: OSError ({type(e).__name__}): {e!r}",
flush=True,
)
self._cleanup()
return False
except Exception as e:
logger.warning(
"Socket send 异常 (%s): %s(将重连)", type(e).__name__, e
)
print(
f"[SocketClient] send_command: 异常 ({type(e).__name__}): {e!r}",
flush=True,
)
self._cleanup()
return False
# 发送命令并重试
def send_command_with_retry(self, command) -> bool:
"""失败后清理连接并按 reconnect_interval 重试max_retries=-1 时直到发送成功。"""
unlimited = self.max_reconnect_attempts < 0
cap = max(1, self.max_reconnect_attempts) if not unlimited else None
attempt = 0
while True:
attempt += 1
self._cleanup()
if self.send_command(command):
if attempt > 1:
print(
f"[SocketClient] 重试后发送成功(第 {attempt} 次)",
flush=True,
)
logger.info("Socket 重连后命令已发送(第 %s 次尝试)", attempt)
return True
if not unlimited and cap is not None and attempt >= cap:
logger.warning(
"Socket 已达 max_retries=%s,本次命令未送达,稍后可再试",
self.max_reconnect_attempts,
)
print(
"[SocketClient] 已达最大重试次数,本次命令未送达(可稍后重试)",
flush=True,
)
return False
# 无限重试时每 10 次打一条日志,避免刷屏
if unlimited and attempt % 10 == 1:
logger.warning(
"Socket 发送失败,%ss 后第 %s 次重连重试…",
self.reconnect_interval,
attempt,
)
print(
f"[SocketClient] 发送失败,{self.reconnect_interval}s 后重试 "
f"(第 {attempt} 次)",
flush=True,
)
time.sleep(self.reconnect_interval)
# 上下文管理器入口
def __enter__(self):
self.connect()
return self
# 上下文管理器出口
def __exit__(self, exc_type, exc_val, exc_tb):
self.disconnect()
if __name__ == "__main__":
from voice_drone.core.configuration import SYSTEM_SOCKET_SERVER_CONFIG
from voice_drone.core.command import Command
config = SYSTEM_SOCKET_SERVER_CONFIG
client = SocketClient(config)
client.connect()
command = Command.create("takeoff", 1)
client.send_command(command)
client.disconnect()