dougbot/dougbot/bridge/ws_client.py
roberts 9aa0abbf59 Phase 1+2: Doug connects, chats, brain loop (movement WIP)
- 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>
2026-03-30 10:30:39 -05:00

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))