File size: 3,221 Bytes
02eac4b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
/**
 * Producer client for controlling robots in LeRobot Arena
 */

import { RoboticsClientCore } from './core.js';
import type {
  JointData,
  WebSocketMessage,
  JointUpdateMessage,
  ClientOptions,
} from './types.js';

export class RoboticsProducer extends RoboticsClientCore {
  constructor(baseUrl = 'http://localhost:8000', options: ClientOptions = {}) {
    super(baseUrl, options);
  }

  // ============= PRODUCER CONNECTION =============

  async connect(workspaceId: string, roomId: string, participantId?: string): Promise<boolean> {
    return this.connectToRoom(workspaceId, roomId, 'producer', participantId);
  }

  // ============= PRODUCER METHODS =============

  async sendJointUpdate(joints: JointData[]): Promise<void> {
    if (!this.connected || !this.websocket) {
      throw new Error('Must be connected to send joint updates');
    }

    const message: JointUpdateMessage = {
      type: 'joint_update',
      data: joints,
      timestamp: new Date().toISOString(),
    };

    this.websocket.send(JSON.stringify(message));
  }

  async sendStateSync(state: Record<string, number>): Promise<void> {
    if (!this.connected || !this.websocket) {
      throw new Error('Must be connected to send state sync');
    }

    // Convert state object to joint updates format
    const joints: JointData[] = Object.entries(state).map(([name, value]) => ({
      name,
      value,
    }));

    await this.sendJointUpdate(joints);
  }

  async sendEmergencyStop(reason = 'Emergency stop'): Promise<void> {
    if (!this.connected || !this.websocket) {
      throw new Error('Must be connected to send emergency stop');
    }

    const message = {
      type: 'emergency_stop' as const,
      reason,
      timestamp: new Date().toISOString(),
    };

    this.websocket.send(JSON.stringify(message));
  }

  // ============= MESSAGE HANDLING =============

  protected override handleRoleSpecificMessage(message: WebSocketMessage): void {
    switch (message.type) {
      case 'emergency_stop':
        console.warn(`🚨 Emergency stop: ${message.reason || 'Unknown reason'}`);
        this.handleError(`Emergency stop: ${message.reason || 'Unknown reason'}`);
        break;
      case 'error':
        console.error(`Server error: ${message.message}`);
        this.handleError(message.message);
        break;
      default:
        console.warn(`Unknown message type for producer: ${message.type}`);
    }
  }

  // ============= UTILITY METHODS =============

  /**
   * Create a room and automatically connect as producer
   */
  static async createAndConnect(
    baseUrl = 'http://localhost:8000',
    workspaceId?: string,
    roomId?: string,
    participantId?: string
  ): Promise<RoboticsProducer> {
    const producer = new RoboticsProducer(baseUrl);
    
    const roomData = await producer.createRoom(workspaceId, roomId);
    const connected = await producer.connect(roomData.workspaceId, roomData.roomId, participantId);
    
    if (!connected) {
      throw new Error('Failed to connect as producer');
    }
    
    return producer;
  }

  /**
   * Get the current room ID (useful when auto-created)
   */
  get currentRoomId(): string | null {
    return this.roomId;
  }
}