Files
AirTrapMine/src/GUI/websocket_client.py
T

156 lines
5.5 KiB
Python
Raw Normal View History

2026-08-18 15:56:16 +08:00
import asyncio
import websockets
import json
import sys
WS_URL = "ws://127.0.0.1:8765"
SYSTEM_ID = 2
GOTO_POSITION = [24.1234567, 120.1234567]
class WebSocketClient:
def __init__(self, ws_url=WS_URL, system_id=SYSTEM_ID):
self.ws_url = ws_url
if isinstance(system_id, bool) or not isinstance(system_id, int):
raise ValueError("system_id 必須為整數")
self.system_id = system_id
def process_websocket_message(self, data):
"""解析 server 的 Essential/Full UAV 狀態或命令 ACK。"""
if isinstance(data, str):
try:
data = json.loads(data)
except json.JSONDecodeError as e:
print(f"JSON 格式錯誤: {e}")
return
if not isinstance(data, dict):
print(f"未知訊息格式: {data}")
return
if all(key in data for key in ("result", "command", "message")):
print(
f"ACK | Command:{data.get('command', '-')} | "
f"Result:{data.get('result', '-')} | "
f"Message:{data.get('message', '-')}"
)
return
if "s" not in data:
print(f"未知 JSON 訊息: {data}")
return
position = data.get("p")
if isinstance(position, (list, tuple)) and len(position) >= 2:
position_text = f"{position[0]}, {position[1]}"
else:
position_text = "-"
ypr = data.get("ypr")
if isinstance(ypr, (list, tuple)) and len(ypr) >= 3:
yaw, pitch, roll = ypr[0], ypr[1], ypr[2]
else:
yaw, pitch, roll = data.get("y"), None, None
dop = data.get("d")
if isinstance(dop, (list, tuple)) and len(dop) >= 2:
dop_text = f"{dop[0]}/{dop[1]}"
else:
dop_text = "-"
armed = data.get("a")
if armed is None:
armed_text = "-"
else:
armed_text = "ARMED" if bool(armed) else "DISARMED"
def value_or_dash(value, suffix=""):
return f"{value}{suffix}" if value is not None else "-"
print(
f"UAV | ID:{data.get('s', '-')} | "
f"Mode:{value_or_dash(data.get('m'))} | "
f"Arm:{armed_text} | "
f"Battery:{value_or_dash(data.get('b'), '%')} | "
f"Position:{position_text} | "
f"Height:{value_or_dash(data.get('h'), 'm')} | "
f"Velocity:{value_or_dash(data.get('v'), 'm/s')} | "
f"Yaw:{value_or_dash(yaw, 'deg')} | "
f"Pitch:{value_or_dash(pitch, 'deg')} | "
f"Roll:{value_or_dash(roll, 'deg')} | "
f"GPS:{value_or_dash(data.get('g'))} | DOP(H/V):{dop_text}"
)
async def send_commands_loop(self, websocket):
"""從 stdin 讀取按鍵 (輸入後按 Enter) 並發送對應的 JSON 指令"""
loop = asyncio.get_event_loop()
print("輸入 1 = START, 2 = STOP, 3 = GOTO (然後按 Enter)")
while True:
# readline 會包含換行
try:
line = await loop.run_in_executor(None, sys.stdin.readline)
except Exception:
return
if not line:
# EOF
return
cmd = line.strip()
if cmd == '1':
msg = {"s": self.system_id, "c": "START"}
elif cmd == '2':
msg = {"s": self.system_id, "c": "STOP"}
elif cmd == '3':
msg = {
"s": self.system_id,
"c": "GOTO",
"p": list(GOTO_POSITION),
}
else:
print(f"未知指令: {cmd}")
continue
try:
await websocket.send(json.dumps(msg))
print(f"已發送: {msg}")
except Exception as e:
print(f"發送失敗: {e}")
return
async def ws_client_loop(self):
retry_count = 0
max_retries = 5
base_delay = 1.0
while retry_count < max_retries:
try:
async with websockets.connect(self.ws_url) as websocket:
print(f"WebSocket connected: {self.ws_url}")
retry_count = 0
# 啟動後台任務來接收 stdin 指令並發送
send_task = asyncio.create_task(self.send_commands_loop(websocket))
try:
async for message in websocket:
# message 已經是 string
self.process_websocket_message(message)
finally:
send_task.cancel()
await asyncio.gather(send_task, return_exceptions=True)
except websockets.exceptions.ConnectionClosedError:
print("WebSocket connection closed")
break
except Exception as e:
retry_count += 1
delay = base_delay * (2 ** min(retry_count, 4))
print(f"WebSocket connection error: {e}, retrying in {delay}s (attempt {retry_count}/{max_retries})")
await asyncio.sleep(delay)
print("WebSocket client stopped after maximum retries")
async def start(self):
"""啟動 WebSocket 客戶端"""
await self.ws_client_loop()
if __name__ == "__main__":
client = WebSocketClient()
asyncio.run(client.start())