- Hybrid Python/Node.js architecture with WebSocket bridge - PySide6 desktop app with smoky blue futuristic theme - bedrock-protocol connection (offline + Xbox Live auth + Realms) - Ollama integration with lean persona prompt - 40 personality traits (15 sliders + 23 quirks + 2 toggles) - Chat working in-game with personality - Brain loop with decision engine - Movement code (needs mineflayer-bedrock for proper server-auth) - Entity tracking framework - RakNet protocol 11 patch for newer BDS Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
139 lines
4.6 KiB
Python
139 lines
4.6 KiB
Python
"""
|
|
WebSocket client for communicating with the Node.js bridge.
|
|
Uses QWebSocket for native Qt event loop integration.
|
|
"""
|
|
|
|
import json
|
|
from typing import Optional, Callable
|
|
from PySide6.QtCore import QObject, Signal, QTimer, QUrl
|
|
from PySide6.QtWebSockets import QWebSocket
|
|
|
|
from dougbot.bridge.protocol import (
|
|
RequestMessage,
|
|
ResponseMessage,
|
|
EventMessage,
|
|
parse_bridge_message,
|
|
)
|
|
from dougbot.utils.logging import get_logger
|
|
|
|
log = get_logger("bridge.ws_client")
|
|
|
|
|
|
class BridgeWSClient(QObject):
|
|
"""WebSocket client that connects to the Node.js bridge."""
|
|
|
|
# Signals
|
|
connected = Signal()
|
|
disconnected = Signal()
|
|
event_received = Signal(str, dict) # event_name, data
|
|
error_occurred = Signal(str)
|
|
|
|
def __init__(self, parent=None):
|
|
super().__init__(parent)
|
|
self._ws = QWebSocket("", parent=self)
|
|
self._url: Optional[str] = None
|
|
self._pending_requests: dict[str, Callable] = {}
|
|
self._reconnect_timer = QTimer(self)
|
|
self._reconnect_timer.setInterval(3000)
|
|
self._reconnect_timer.timeout.connect(self._try_reconnect)
|
|
self._should_reconnect = False
|
|
|
|
# Connect QWebSocket signals
|
|
self._ws.connected.connect(self._on_connected)
|
|
self._ws.disconnected.connect(self._on_disconnected)
|
|
self._ws.textMessageReceived.connect(self._on_message)
|
|
self._ws.errorOccurred.connect(self._on_error)
|
|
|
|
def connect_to_bridge(self, port: int, host: str = "127.0.0.1") -> None:
|
|
"""Connect to the Node.js bridge WebSocket server."""
|
|
self._url = f"ws://{host}:{port}"
|
|
self._should_reconnect = True
|
|
log.info(f"Connecting to bridge at {self._url}")
|
|
self._ws.open(QUrl(self._url))
|
|
|
|
def disconnect_from_bridge(self) -> None:
|
|
"""Disconnect from the bridge."""
|
|
self._should_reconnect = False
|
|
self._reconnect_timer.stop()
|
|
self._ws.close()
|
|
|
|
def send_request(
|
|
self,
|
|
action: str,
|
|
params: dict | None = None,
|
|
callback: Callable[[ResponseMessage], None] | None = None,
|
|
) -> str:
|
|
"""
|
|
Send a request to the bridge.
|
|
|
|
Args:
|
|
action: The action to perform
|
|
params: Action parameters
|
|
callback: Optional callback for the response
|
|
|
|
Returns:
|
|
Request ID
|
|
"""
|
|
request = RequestMessage(action=action, params=params or {})
|
|
|
|
if callback:
|
|
self._pending_requests[request.id] = callback
|
|
|
|
self._ws.sendTextMessage(request.to_json())
|
|
log.debug(f"Sent request: {action} (id={request.id[:8]})")
|
|
return request.id
|
|
|
|
def send_chat(self, message: str) -> None:
|
|
"""Convenience method to send a chat message."""
|
|
self.send_request("send_chat", {"message": message})
|
|
|
|
def get_status(self, callback: Callable[[ResponseMessage], None]) -> None:
|
|
"""Get current bot status from the bridge."""
|
|
self.send_request("status", {}, callback)
|
|
|
|
def is_connected(self) -> bool:
|
|
"""Check if connected to the bridge."""
|
|
from PySide6.QtNetwork import QAbstractSocket
|
|
return self._ws.state() == QAbstractSocket.SocketState.ConnectedState
|
|
|
|
def _on_connected(self) -> None:
|
|
log.info("Connected to bridge WebSocket")
|
|
self._reconnect_timer.stop()
|
|
self.connected.emit()
|
|
|
|
def _on_disconnected(self) -> None:
|
|
log.info("Disconnected from bridge WebSocket")
|
|
self.disconnected.emit()
|
|
|
|
if self._should_reconnect:
|
|
log.info("Will attempt reconnection in 3 seconds...")
|
|
self._reconnect_timer.start()
|
|
|
|
def _on_message(self, raw: str) -> None:
|
|
"""Handle incoming message from the bridge."""
|
|
message = parse_bridge_message(raw)
|
|
if message is None:
|
|
log.warn(f"Failed to parse bridge message: {raw[:100]}")
|
|
return
|
|
|
|
if isinstance(message, ResponseMessage):
|
|
# Match to pending request
|
|
callback = self._pending_requests.pop(message.id, None)
|
|
if callback:
|
|
callback(message)
|
|
else:
|
|
log.debug(f"Response for unknown request: {message.id[:8]}")
|
|
|
|
elif isinstance(message, EventMessage):
|
|
log.debug(f"Event: {message.event}")
|
|
self.event_received.emit(message.event, message.data)
|
|
|
|
def _on_error(self, error) -> None:
|
|
error_msg = self._ws.errorString()
|
|
log.error(f"WebSocket error: {error_msg}")
|
|
self.error_occurred.emit(error_msg)
|
|
|
|
def _try_reconnect(self) -> None:
|
|
if self._url and self._should_reconnect:
|
|
log.debug("Attempting reconnection...")
|
|
self._ws.open(QUrl(self._url))
|