You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
195 lines
5.6 KiB
195 lines
5.6 KiB
extends Node
|
|
## WebSocket client that connects to teleop_server.py on the robot.
|
|
## Sends body tracking data as JSON, receives JPEG webcam frames as binary.
|
|
##
|
|
## Protocol:
|
|
## Client → Server: JSON text messages with tracking data
|
|
## Server → Client: Binary messages with JPEG webcam frames
|
|
##
|
|
## No SSL required — raw WebSocket over local network.
|
|
|
|
## Emitted when a JPEG webcam frame is received from the server.
|
|
## Camera IDs: 0=head, 1=left_wrist, 2=right_wrist
|
|
signal webcam_frame_received(jpeg_bytes: PackedByteArray) ## Legacy (head only)
|
|
signal webcam_frame_head(jpeg_bytes: PackedByteArray)
|
|
signal webcam_frame_left(jpeg_bytes: PackedByteArray)
|
|
signal webcam_frame_right(jpeg_bytes: PackedByteArray)
|
|
|
|
## Emitted when connection state changes.
|
|
signal connection_state_changed(connected: bool)
|
|
|
|
## Server address — robot's IP and teleop_server.py port
|
|
@export var server_host: String = "10.0.0.64"
|
|
@export var server_port: int = 8765
|
|
@export var auto_connect: bool = true
|
|
|
|
## Reconnection settings
|
|
@export var reconnect_delay_sec: float = 2.0
|
|
@export var max_reconnect_attempts: int = 0 # 0 = unlimited
|
|
|
|
## Connection state
|
|
var ws := WebSocketPeer.new()
|
|
var is_connected: bool = false
|
|
var _reconnect_timer: float = 0.0
|
|
var _reconnect_attempts: int = 0
|
|
var _was_connected: bool = false
|
|
var _pending_data: Array = []
|
|
|
|
## Stats
|
|
var messages_sent: int = 0
|
|
var messages_received: int = 0
|
|
var bytes_sent: int = 0
|
|
var bytes_received: int = 0
|
|
var last_send_time: int = 0
|
|
var last_receive_time: int = 0
|
|
|
|
|
|
func _ready() -> void:
|
|
if auto_connect:
|
|
connect_to_server()
|
|
|
|
|
|
func _process(delta: float) -> void:
|
|
ws.poll()
|
|
|
|
var state := ws.get_ready_state()
|
|
|
|
match state:
|
|
WebSocketPeer.STATE_OPEN:
|
|
if not is_connected:
|
|
is_connected = true
|
|
_was_connected = true
|
|
_reconnect_attempts = 0
|
|
print("[TeleopClient] Connected to ws://%s:%d" % [server_host, server_port])
|
|
connection_state_changed.emit(true)
|
|
|
|
# Process incoming messages
|
|
while ws.get_available_packet_count() > 0:
|
|
var packet := ws.get_packet()
|
|
bytes_received += packet.size()
|
|
messages_received += 1
|
|
last_receive_time = Time.get_ticks_msec()
|
|
|
|
# Check if this is a binary (JPEG) or text message
|
|
if ws.was_string_packet():
|
|
_handle_text_message(packet.get_string_from_utf8())
|
|
else:
|
|
# Binary = 1 byte camera_id + JPEG data
|
|
if packet.size() > 1:
|
|
var cam_id := packet[0]
|
|
var jpeg_data := packet.slice(1)
|
|
match cam_id:
|
|
0:
|
|
webcam_frame_head.emit(jpeg_data)
|
|
webcam_frame_received.emit(jpeg_data)
|
|
1:
|
|
webcam_frame_left.emit(jpeg_data)
|
|
2:
|
|
webcam_frame_right.emit(jpeg_data)
|
|
_:
|
|
webcam_frame_received.emit(packet)
|
|
|
|
# Send any pending tracking data
|
|
for data in _pending_data:
|
|
_send_json(data)
|
|
_pending_data.clear()
|
|
|
|
WebSocketPeer.STATE_CLOSING:
|
|
pass # Wait for close to complete
|
|
|
|
WebSocketPeer.STATE_CLOSED:
|
|
if is_connected or _was_connected:
|
|
var code := ws.get_close_code()
|
|
var reason := ws.get_close_reason()
|
|
print("[TeleopClient] Disconnected (code=%d, reason=%s)" % [code, reason])
|
|
is_connected = false
|
|
connection_state_changed.emit(false)
|
|
|
|
# Auto-reconnect
|
|
if auto_connect and _was_connected:
|
|
_reconnect_timer -= delta
|
|
if _reconnect_timer <= 0:
|
|
_reconnect_timer = reconnect_delay_sec
|
|
_attempt_reconnect()
|
|
|
|
WebSocketPeer.STATE_CONNECTING:
|
|
pass # Still connecting
|
|
|
|
|
|
func connect_to_server() -> void:
|
|
var url := "ws://%s:%d" % [server_host, server_port]
|
|
print("[TeleopClient] Connecting to %s..." % url)
|
|
var err := ws.connect_to_url(url)
|
|
if err != OK:
|
|
printerr("[TeleopClient] Failed to initiate connection: ", err)
|
|
|
|
|
|
func disconnect_from_server() -> void:
|
|
_was_connected = false
|
|
ws.close()
|
|
|
|
|
|
func _attempt_reconnect() -> void:
|
|
if max_reconnect_attempts > 0:
|
|
_reconnect_attempts += 1
|
|
if _reconnect_attempts > max_reconnect_attempts:
|
|
print("[TeleopClient] Max reconnect attempts reached, giving up")
|
|
_was_connected = false
|
|
return
|
|
print("[TeleopClient] Reconnect attempt %d/%d..." % [_reconnect_attempts, max_reconnect_attempts])
|
|
else:
|
|
_reconnect_attempts += 1
|
|
if _reconnect_attempts % 5 == 1: # Log every 5th attempt to avoid spam
|
|
print("[TeleopClient] Reconnect attempt %d..." % _reconnect_attempts)
|
|
|
|
connect_to_server()
|
|
|
|
|
|
## Called by body_tracker via signal when new tracking data is ready.
|
|
func _on_tracking_data(data: Dictionary) -> void:
|
|
if is_connected:
|
|
_send_json(data)
|
|
# Don't buffer if not connected — tracking data is perishable
|
|
|
|
|
|
func _send_json(data: Dictionary) -> void:
|
|
var json_str := JSON.stringify(data)
|
|
var err := ws.send_text(json_str)
|
|
if err == OK:
|
|
messages_sent += 1
|
|
bytes_sent += json_str.length()
|
|
last_send_time = Time.get_ticks_msec()
|
|
else:
|
|
printerr("[TeleopClient] Failed to send: ", err)
|
|
|
|
|
|
func _handle_text_message(text: String) -> void:
|
|
# Server may send JSON status/config messages
|
|
var json := JSON.new()
|
|
var err := json.parse(text)
|
|
if err != OK:
|
|
printerr("[TeleopClient] Invalid JSON from server: ", text.left(100))
|
|
return
|
|
|
|
var data: Dictionary = json.data
|
|
var msg_type: String = data.get("type", "")
|
|
|
|
match msg_type:
|
|
"config":
|
|
print("[TeleopClient] Server config: ", data)
|
|
"status":
|
|
print("[TeleopClient] Server status: ", data.get("message", ""))
|
|
_:
|
|
print("[TeleopClient] Unknown message type: ", msg_type)
|
|
|
|
|
|
## Get connection statistics as a dictionary
|
|
func get_stats() -> Dictionary:
|
|
return {
|
|
"connected": is_connected,
|
|
"messages_sent": messages_sent,
|
|
"messages_received": messages_received,
|
|
"bytes_sent": bytes_sent,
|
|
"bytes_received": bytes_received,
|
|
"reconnect_attempts": _reconnect_attempts,
|
|
}
|