blanchon commited on
Commit
1e7b565
·
1 Parent(s): 3367d1b
Dockerfile CHANGED
@@ -41,7 +41,8 @@ ENV PYTHONUNBUFFERED=1 \
41
  PYTHONDONTWRITEBYTECODE=1 \
42
  UV_SYSTEM_PYTHON=1 \
43
  UV_COMPILE_BYTECODE=1 \
44
- UV_CACHE_DIR=/tmp/uv-cache
 
45
 
46
  # Install system dependencies needed for video processing
47
  RUN apt-get update && apt-get install -y \
@@ -106,12 +107,12 @@ WORKDIR /app
106
  # Add virtual environment to PATH
107
  ENV PATH="/app/server/.venv/bin:$PATH"
108
 
109
- # Expose port 7860 (HuggingFace Spaces default)
110
- EXPOSE 7860
111
 
112
  # Health check
113
  HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
114
- CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:7860/health')" || exit 1
115
 
116
  # Start the FastAPI server (serves both frontend and backend)
117
- CMD ["sh", "-c", "cd server && SERVE_FRONTEND=true uv run python launch_with_ui.py --host 0.0.0.0 --port 7860"]
 
41
  PYTHONDONTWRITEBYTECODE=1 \
42
  UV_SYSTEM_PYTHON=1 \
43
  UV_COMPILE_BYTECODE=1 \
44
+ UV_CACHE_DIR=/tmp/uv-cache \
45
+ PORT=8000
46
 
47
  # Install system dependencies needed for video processing
48
  RUN apt-get update && apt-get install -y \
 
107
  # Add virtual environment to PATH
108
  ENV PATH="/app/server/.venv/bin:$PATH"
109
 
110
+ # Expose the configured port (default 8000)
111
+ EXPOSE ${PORT}
112
 
113
  # Health check
114
  HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
115
+ CMD python -c "import urllib.request; urllib.request.urlopen(f'http://localhost:${PORT}/health')" || exit 1
116
 
117
  # Start the FastAPI server (serves both frontend and backend)
118
+ CMD ["sh", "-c", "cd server && SERVE_FRONTEND=true uv run python launch_with_ui.py --host localhost --port ${PORT}"]
README.md CHANGED
@@ -4,7 +4,7 @@ emoji: 🤖
4
  colorFrom: blue
5
  colorTo: purple
6
  sdk: docker
7
- app_port: 7860
8
  suggested_hardware: cpu-upgrade
9
  suggested_storage: small
10
  short_description: Real-time robotics control and video streaming platform
 
4
  colorFrom: blue
5
  colorTo: purple
6
  sdk: docker
7
+ app_port: 8000
8
  suggested_hardware: cpu-upgrade
9
  suggested_storage: small
10
  short_description: Real-time robotics control and video streaming platform
client/COMPARE.md ADDED
@@ -0,0 +1,127 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Client Library API Comparison
2
+
3
+ This table shows the correspondence between the Python and JavaScript/TypeScript client libraries for RobotHub TransportServer.
4
+
5
+ ## 📦 Installation
6
+
7
+ | Python | JavaScript/TypeScript |
8
+ |--------|----------------------|
9
+ | `uv add transport-server-client` | `bun add @robothub/transport-server-client` |
10
+
11
+ ## 🔧 Imports
12
+
13
+ | Python | JavaScript/TypeScript |
14
+ |--------|----------------------|
15
+ | `from transport_server_client import RoboticsProducer` | `import { robotics } from '@robothub/transport-server-client'` |
16
+ | `from transport_server_client import RoboticsConsumer` | `const producer = new robotics.RoboticsProducer()` |
17
+ | `from transport_server_client.video import VideoProducer` | `import { video } from '@robothub/transport-server-client'` |
18
+ | `from transport_server_client.video import VideoConsumer` | `const producer = new video.VideoProducer()` |
19
+
20
+ ## 🤖 Robotics Producer
21
+
22
+ | Operation | Python | JavaScript/TypeScript |
23
+ |-----------|--------|----------------------|
24
+ | **Create instance** | `producer = RoboticsProducer('http://localhost:8000')` | `const producer = new robotics.RoboticsProducer('http://localhost:8000')` |
25
+ | **Connect to room** | `await producer.connect(workspace_id, room_id)` | `await producer.connect(workspaceId, roomId)` |
26
+ | **Create room** | `room_info = await producer.create_room()` | `const { workspaceId, roomId } = await producer.createRoom()` |
27
+ | **Send joint update** | `await producer.send_joint_update(joints)` | `await producer.sendJointUpdate(joints)` |
28
+ | **Send state sync** | `await producer.send_state_sync(state)` | `await producer.sendStateSync(state)` |
29
+ | **Emergency stop** | `await producer.send_emergency_stop(reason)` | `await producer.sendEmergencyStop(reason)` |
30
+ | **List rooms** | `rooms = await producer.list_rooms(workspace_id)` | `const rooms = await producer.listRooms(workspaceId)` |
31
+ | **Delete room** | `await producer.delete_room(workspace_id, room_id)` | `await producer.deleteRoom(workspaceId, roomId)` |
32
+ | **Disconnect** | `await producer.disconnect()` | `await producer.disconnect()` |
33
+
34
+ ## 🤖 Robotics Consumer
35
+
36
+ | Operation | Python | JavaScript/TypeScript |
37
+ |-----------|--------|----------------------|
38
+ | **Create instance** | `consumer = RoboticsConsumer('http://localhost:8000')` | `const consumer = new robotics.RoboticsConsumer('http://localhost:8000')` |
39
+ | **Connect to room** | `await consumer.connect(workspace_id, room_id)` | `await consumer.connect(workspaceId, roomId)` |
40
+ | **Get current state** | `state = await consumer.get_state_sync()` | `const state = await consumer.getStateSyncAsync()` |
41
+ | **Joint update callback** | `consumer.on_joint_update(callback)` | `consumer.onJointUpdate(callback)` |
42
+ | **State sync callback** | `consumer.on_state_sync(callback)` | `consumer.onStateSync(callback)` |
43
+ | **Error callback** | `consumer.on_error(callback)` | `consumer.onError(callback)` |
44
+ | **Connected callback** | `consumer.on_connected(callback)` | `consumer.onConnected(callback)` |
45
+ | **Disconnected callback** | `consumer.on_disconnected(callback)` | `consumer.onDisconnected(callback)` |
46
+ | **Disconnect** | `await consumer.disconnect()` | `await consumer.disconnect()` |
47
+
48
+ ## 📹 Video Producer
49
+
50
+ | Operation | Python | JavaScript/TypeScript |
51
+ |-----------|--------|----------------------|
52
+ | **Create instance** | `producer = VideoProducer('http://localhost:8000')` | `const producer = new video.VideoProducer('http://localhost:8000')` |
53
+ | **Connect to room** | `await producer.connect(workspace_id, room_id)` | `await producer.connect(workspaceId, roomId)` |
54
+ | **Start camera** | `await producer.start_camera(config)` | `await producer.startCamera(constraints)` |
55
+ | **Start screen share** | `await producer.start_screen_share()` | `await producer.startScreenShare()` |
56
+ | **Stop streaming** | `await producer.stop_streaming()` | `await producer.stopStreaming()` |
57
+ | **Update config** | `await producer.update_video_config(config)` | `await producer.updateVideoConfig(config)` |
58
+ | **Disconnect** | `await producer.disconnect()` | `await producer.disconnect()` |
59
+
60
+ ## 📹 Video Consumer
61
+
62
+ | Operation | Python | JavaScript/TypeScript |
63
+ |-----------|--------|----------------------|
64
+ | **Create instance** | `consumer = VideoConsumer('http://localhost:8000')` | `const consumer = new video.VideoConsumer('http://localhost:8000')` |
65
+ | **Connect to room** | `await consumer.connect(workspace_id, room_id)` | `await consumer.connect(workspaceId, roomId)` |
66
+ | **Start receiving** | `await consumer.start_receiving()` | `await consumer.startReceiving()` |
67
+ | **Stop receiving** | `await consumer.stop_receiving()` | `await consumer.stopReceiving()` |
68
+ | **Attach to video element** | N/A (Python) | `consumer.attachToVideoElement(videoElement)` |
69
+ | **Frame callback** | `consumer.on_frame_update(callback)` | `consumer.onFrameUpdate(callback)` |
70
+ | **Stream started callback** | `consumer.on_stream_started(callback)` | `consumer.onStreamStarted(callback)` |
71
+ | **Stream stopped callback** | `consumer.on_stream_stopped(callback)` | `consumer.onStreamStopped(callback)` |
72
+ | **Disconnect** | `await consumer.disconnect()` | `await consumer.disconnect()` |
73
+
74
+ ## ⚡ Factory Functions
75
+
76
+ | Operation | Python | JavaScript/TypeScript |
77
+ |-----------|--------|----------------------|
78
+ | **Quick producer** | `producer = await create_producer_client(url)` | `const producer = await robotics.createProducerClient(url)` |
79
+ | **Quick consumer** | `consumer = await create_consumer_client(room_id, url)` | `const consumer = await robotics.createConsumerClient(workspaceId, roomId, url)` |
80
+
81
+ ## 🔧 Context Managers / Lifecycle
82
+
83
+ | Operation | Python | JavaScript/TypeScript |
84
+ |-----------|--------|----------------------|
85
+ | **Auto cleanup** | `async with RoboticsProducer(url) as producer:` | No built-in equivalent |
86
+ | **Check connection** | `producer.is_connected()` | `producer.isConnected()` |
87
+ | **Connection info** | `info = producer.get_connection_info()` | `const info = producer.getConnectionInfo()` |
88
+
89
+ ## 📝 Data Formats
90
+
91
+ ### Joint Data
92
+
93
+ | Python | JavaScript/TypeScript |
94
+ |--------|----------------------|
95
+ | `{'name': 'shoulder', 'value': 45.0}` | `{ name: 'shoulder', value: 45.0 }` |
96
+ | `[{'name': 'shoulder', 'value': 45.0}]` | `[{ name: 'shoulder', value: 45.0 }]` |
97
+
98
+ ### State Data
99
+
100
+ | Python | JavaScript/TypeScript |
101
+ |--------|----------------------|
102
+ | `{'shoulder': 45.0, 'elbow': -30.0}` | `{ shoulder: 45.0, elbow: -30.0 }` |
103
+
104
+ ### Room Info Response
105
+
106
+ | Python | JavaScript/TypeScript |
107
+ |--------|----------------------|
108
+ | `{'workspace_id': 'uuid', 'room_id': 'uuid'}` | `{ workspaceId: 'uuid', roomId: 'uuid' }` |
109
+
110
+ ## 🔄 Naming Conventions
111
+
112
+ | Python (snake_case) | JavaScript/TypeScript (camelCase) |
113
+ |---------------------|-----------------------------------|
114
+ | `send_joint_update` | `sendJointUpdate` |
115
+ | `send_state_sync` | `sendStateSync` |
116
+ | `get_state_sync` | `getStateSyncAsync` |
117
+ | `on_joint_update` | `onJointUpdate` |
118
+ | `create_room` | `createRoom` |
119
+ | `list_rooms` | `listRooms` |
120
+ | `workspace_id` | `workspaceId` |
121
+ | `room_id` | `roomId` |
122
+ | `start_camera` | `startCamera` |
123
+ | `stop_streaming` | `stopStreaming` |
124
+
125
+ ---
126
+
127
+ **Both libraries provide the same functionality with language-appropriate conventions!** 🤖✨
client/js/tests/README.md ADDED
@@ -0,0 +1,228 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # RobotHub TransportServer JavaScript Client Tests
2
+
3
+ ## Overview
4
+
5
+ This directory contains comprehensive tests for the RobotHub TransportServer JavaScript/TypeScript client library, mirroring the Python test structure. The tests are built using [Bun's test framework](https://bun.sh/docs/test/writing) and provide full coverage of both robotics and video functionality.
6
+
7
+ ## Test Structure
8
+
9
+ The test suite is organized to match the Python client tests:
10
+
11
+ ```
12
+ tests/
13
+ ├── setup.ts # Test utilities and helpers (equivalent to conftest.py)
14
+ ├── producer.test.ts # RoboticsProducer tests
15
+ ├── consumer.test.ts # RoboticsConsumer tests
16
+ ├── factory.test.ts # Factory function tests
17
+ ├── integration.test.ts # Integration tests
18
+ ├── rest-api.test.ts # REST API tests
19
+ ├── video-client.test.ts # Video client tests
20
+ └── README.md # This file
21
+ ```
22
+
23
+ ## Running Tests
24
+
25
+ ### Prerequisites
26
+
27
+ 1. **Server Running**: Ensure the RobotHub TransportServer is running on `http://localhost:8000`
28
+ 2. **Dependencies**: Install dependencies with `bun install`
29
+
30
+ ### Run All Tests
31
+
32
+ ```bash
33
+ # From the js client directory
34
+ bun test
35
+
36
+ # Or using npm script
37
+ bun run test
38
+ ```
39
+
40
+ ### Run Specific Test Files
41
+
42
+ ```bash
43
+ # Run only producer tests
44
+ bun test tests/producer.test.ts
45
+
46
+ # Run only integration tests
47
+ bun test tests/integration.test.ts
48
+
49
+ # Run with verbose output
50
+ bun test --verbose
51
+ ```
52
+
53
+ ## Test Categories
54
+
55
+ ### 1. Robotics Producer Tests (`producer.test.ts`)
56
+ - ✅ Basic connection and disconnection
57
+ - ✅ Connection info validation
58
+ - ✅ Joint updates and state synchronization
59
+ - ✅ Emergency stop functionality
60
+ - ✅ Event callbacks (connected, disconnected, error)
61
+ - ✅ Error handling for disconnected operations
62
+ - ✅ Multiple room connections
63
+ - ✅ Custom participant IDs
64
+ - ✅ Large data handling
65
+ - ✅ High-frequency updates
66
+
67
+ ### 2. Robotics Consumer Tests (`consumer.test.ts`)
68
+ - ✅ Basic connection and disconnection
69
+ - ✅ Connection info validation
70
+ - ✅ State synchronization retrieval
71
+ - ✅ Event callbacks setup
72
+ - ✅ Multiple consumers in same room
73
+ - ✅ Receiving state sync and joint updates
74
+ - ✅ Emergency stop reception
75
+ - ✅ Custom participant IDs
76
+ - ✅ Reconnection scenarios
77
+ - ✅ State persistence after producer updates
78
+
79
+ ### 3. Factory Function Tests (`factory.test.ts`)
80
+ - ✅ Client creation by role
81
+ - ✅ Invalid role handling
82
+ - ✅ Auto room creation for producers
83
+ - ✅ Specific room connection
84
+ - ✅ Producer-consumer pair creation
85
+ - ✅ Default URL handling
86
+ - ✅ Multiple producer management
87
+ - ✅ Nonexistent room error handling
88
+
89
+ ### 4. Integration Tests (`integration.test.ts`)
90
+ - ✅ Full producer-consumer workflow
91
+ - ✅ Multiple consumers receiving same data
92
+ - ✅ Emergency stop propagation
93
+ - ✅ Producer reconnection scenarios
94
+ - ✅ Late-joining consumers
95
+ - ✅ Room state persistence
96
+ - ✅ High-frequency update handling
97
+
98
+ ### 5. REST API Tests (`rest-api.test.ts`)
99
+ - ✅ Room listing (empty and populated)
100
+ - ✅ Room creation (auto and custom IDs)
101
+ - ✅ Room information retrieval
102
+ - ✅ Room state retrieval
103
+ - ✅ Room deletion
104
+ - ✅ Error handling for nonexistent rooms
105
+
106
+ ### 6. Video Client Tests (`video-client.test.ts`)
107
+ - ✅ Type definitions validation
108
+ - ✅ Producer and consumer creation
109
+ - ✅ Room creation and listing (when server available)
110
+ - ✅ Connection validation
111
+ - ✅ Factory function existence
112
+ - ✅ Mock frame source functionality
113
+ - ✅ Configuration type validation
114
+
115
+ ## Test Results Summary
116
+
117
+ ```
118
+ ✅ 69 tests passed
119
+ ❌ 0 tests failed
120
+ 🔍 187 expect() calls
121
+ ⏱️ Runtime: ~4 seconds
122
+ ```
123
+
124
+ ## API Correspondence
125
+
126
+ The JavaScript tests mirror the Python test structure, ensuring API parity:
127
+
128
+ | Python Test | JavaScript Test | Coverage |
129
+ |-------------|-----------------|----------|
130
+ | `test_producer.py` | `producer.test.ts` | ✅ Complete |
131
+ | `test_consumer.py` | `consumer.test.ts` | ✅ Complete |
132
+ | `test_factory_functions.py` | `factory.test.ts` | ✅ Complete |
133
+ | `test_integration.py` | `integration.test.ts` | ✅ Complete |
134
+ | `test_rest_api.py` | `rest-api.test.ts` | ✅ Complete |
135
+ | `test_video_client.py` | `video-client.test.ts` | ✅ Complete |
136
+ | `conftest.py` | `setup.ts` | ✅ Complete |
137
+
138
+ ## Test Features
139
+
140
+ ### Async/Await Support
141
+ All tests use modern async/await patterns with proper cleanup:
142
+
143
+ ```typescript
144
+ test("producer connection", async () => {
145
+ const { workspaceId, roomId } = await producer.createRoom();
146
+ await producer.connect(workspaceId, roomId);
147
+ expect(producer.isConnected()).toBe(true);
148
+ await producer.disconnect();
149
+ });
150
+ ```
151
+
152
+ ### Message Collection
153
+ Tests use a `MessageCollector` utility for testing callbacks:
154
+
155
+ ```typescript
156
+ const updateCollector = new MessageCollector(1);
157
+ consumer.onJointUpdate(updateCollector.collect);
158
+ await producer.sendJointUpdate(joints);
159
+ const updates = await updateCollector.waitForMessages(2000);
160
+ expect(updates.length).toBeGreaterThanOrEqual(1);
161
+ ```
162
+
163
+ ### Resource Management
164
+ Automatic cleanup prevents test interference:
165
+
166
+ ```typescript
167
+ afterEach(async () => {
168
+ if (producer.isConnected()) {
169
+ await producer.disconnect();
170
+ }
171
+ await roomManager.cleanup(producer);
172
+ });
173
+ ```
174
+
175
+ ### Error Testing
176
+ Comprehensive error scenario coverage:
177
+
178
+ ```typescript
179
+ test("send without connection", async () => {
180
+ await expect(producer.sendJointUpdate([]))
181
+ .rejects.toThrow("Must be connected");
182
+ });
183
+ ```
184
+
185
+ ## Debugging Tests
186
+
187
+ ### Enable Debug Logging
188
+ ```bash
189
+ # Run with debug output
190
+ DEBUG=* bun test
191
+
192
+ # Or specific modules
193
+ DEBUG=robotics:* bun test
194
+ ```
195
+
196
+ ### Test Individual Scenarios
197
+ ```bash
198
+ # Test specific functionality
199
+ bun test --grep "emergency stop"
200
+ bun test --grep "multiple consumers"
201
+ ```
202
+
203
+ ### Server Connectivity Issues
204
+ If tests fail due to server connectivity:
205
+
206
+ 1. Ensure server is running: `curl http://localhost:8000/health`
207
+ 2. Check server logs for errors
208
+ 3. Verify WebSocket connections are allowed
209
+ 4. Try running tests with longer timeouts
210
+
211
+ ## Contributing
212
+
213
+ When adding new tests:
214
+
215
+ 1. Follow the existing naming conventions
216
+ 2. Add proper cleanup in `afterEach` blocks
217
+ 3. Use `MessageCollector` for callback testing
218
+ 4. Test both success and error scenarios
219
+ 5. Update this README with new test descriptions
220
+
221
+ ## Performance Notes
222
+
223
+ - Tests run in parallel by default
224
+ - Average test suite runtime: ~4 seconds
225
+ - Individual test timeouts: 10 seconds
226
+ - Message collection timeouts: 2-5 seconds
227
+
228
+ The JavaScript test suite provides comprehensive validation that the TypeScript/JavaScript client maintains full API compatibility with the Python client while leveraging modern JavaScript testing practices.
client/js/tests/consumer.test.ts ADDED
@@ -0,0 +1,316 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Tests for RoboticsConsumer - equivalent to Python's test_consumer.py
3
+ */
4
+ import { test, expect, describe, beforeEach, afterEach } from "bun:test";
5
+ import { robotics } from "../src/index";
6
+ import { TEST_SERVER_URL, TestRoomManager, MessageCollector, sleep, assertIsConnected, assertIsDisconnected } from "./setup";
7
+
8
+ const { RoboticsProducer, RoboticsConsumer } = robotics;
9
+
10
+ describe("RoboticsConsumer", () => {
11
+ let consumer: robotics.RoboticsConsumer;
12
+ let producer: robotics.RoboticsProducer;
13
+ let roomManager: TestRoomManager;
14
+
15
+ beforeEach(() => {
16
+ consumer = new RoboticsConsumer(TEST_SERVER_URL);
17
+ producer = new RoboticsProducer(TEST_SERVER_URL);
18
+ roomManager = new TestRoomManager();
19
+ });
20
+
21
+ afterEach(async () => {
22
+ if (consumer.isConnected()) {
23
+ await consumer.disconnect();
24
+ }
25
+ if (producer.isConnected()) {
26
+ await producer.disconnect();
27
+ }
28
+ await roomManager.cleanup(producer);
29
+ });
30
+
31
+ test("consumer connection", async () => {
32
+ // Create room first
33
+ const { workspaceId, roomId } = await producer.createRoom();
34
+ roomManager.addRoom(workspaceId, roomId);
35
+
36
+ expect(consumer.isConnected()).toBe(false);
37
+
38
+ const success = await consumer.connect(workspaceId, roomId);
39
+ expect(success).toBe(true);
40
+ assertIsConnected(consumer, workspaceId, roomId);
41
+
42
+ const info = consumer.getConnectionInfo();
43
+ expect(info.role).toBe("consumer");
44
+
45
+ await consumer.disconnect();
46
+ assertIsDisconnected(consumer);
47
+ });
48
+
49
+ test("consumer connection info", async () => {
50
+ const { workspaceId, roomId } = await producer.createRoom();
51
+ roomManager.addRoom(workspaceId, roomId);
52
+ await consumer.connect(workspaceId, roomId);
53
+
54
+ const info = consumer.getConnectionInfo();
55
+ expect(info.connected).toBe(true);
56
+ expect(info.room_id).toBe(roomId);
57
+ expect(info.workspace_id).toBe(workspaceId);
58
+ expect(info.role).toBe("consumer");
59
+ expect(info.participant_id).toBeTruthy();
60
+ expect(info.base_url).toBe(TEST_SERVER_URL);
61
+ });
62
+
63
+ test("get state sync", async () => {
64
+ const { workspaceId, roomId } = await producer.createRoom();
65
+ roomManager.addRoom(workspaceId, roomId);
66
+ await consumer.connect(workspaceId, roomId);
67
+
68
+ const state = await consumer.getStateSyncAsync();
69
+ expect(typeof state).toBe("object");
70
+ // Initial state should be empty
71
+ expect(Object.keys(state)).toHaveLength(0);
72
+ });
73
+
74
+ test("consumer callbacks setup", async () => {
75
+ const { workspaceId, roomId } = await producer.createRoom();
76
+ roomManager.addRoom(workspaceId, roomId);
77
+
78
+ let stateSyncCalled = false;
79
+ let jointUpdateCalled = false;
80
+ let errorCalled = false;
81
+ let connectedCalled = false;
82
+ let disconnectedCalled = false;
83
+
84
+ consumer.onStateSync((state) => {
85
+ stateSyncCalled = true;
86
+ });
87
+
88
+ consumer.onJointUpdate((joints) => {
89
+ jointUpdateCalled = true;
90
+ });
91
+
92
+ consumer.onError((error) => {
93
+ errorCalled = true;
94
+ });
95
+
96
+ consumer.onConnected(() => {
97
+ connectedCalled = true;
98
+ });
99
+
100
+ consumer.onDisconnected(() => {
101
+ disconnectedCalled = true;
102
+ });
103
+
104
+ // Connect and test connection callbacks
105
+ await consumer.connect(workspaceId, roomId);
106
+ await sleep(100);
107
+ expect(connectedCalled).toBe(true);
108
+
109
+ await consumer.disconnect();
110
+ await sleep(100);
111
+ expect(disconnectedCalled).toBe(true);
112
+ });
113
+
114
+ test("multiple consumers", async () => {
115
+ const { workspaceId, roomId } = await producer.createRoom();
116
+ roomManager.addRoom(workspaceId, roomId);
117
+
118
+ const consumer1 = new RoboticsConsumer(TEST_SERVER_URL);
119
+ const consumer2 = new RoboticsConsumer(TEST_SERVER_URL);
120
+
121
+ try {
122
+ // Both consumers should be able to connect
123
+ const success1 = await consumer1.connect(workspaceId, roomId);
124
+ const success2 = await consumer2.connect(workspaceId, roomId);
125
+
126
+ expect(success1).toBe(true);
127
+ expect(success2).toBe(true);
128
+ expect(consumer1.isConnected()).toBe(true);
129
+ expect(consumer2.isConnected()).toBe(true);
130
+ } finally {
131
+ if (consumer1.isConnected()) {
132
+ await consumer1.disconnect();
133
+ }
134
+ if (consumer2.isConnected()) {
135
+ await consumer2.disconnect();
136
+ }
137
+ }
138
+ });
139
+
140
+ test("consumer receive state sync", async () => {
141
+ const { workspaceId, roomId } = await producer.createRoom();
142
+ roomManager.addRoom(workspaceId, roomId);
143
+
144
+ const updateCollector = new MessageCollector(1);
145
+ consumer.onJointUpdate(updateCollector.collect);
146
+
147
+ await producer.connect(workspaceId, roomId);
148
+ await consumer.connect(workspaceId, roomId);
149
+
150
+ // Give some time for connection to stabilize
151
+ await sleep(100);
152
+
153
+ // Producer sends state sync (which gets converted to joint updates)
154
+ await producer.sendStateSync({ shoulder: 45.0, elbow: -20.0 });
155
+
156
+ // Wait for message to be received
157
+ const receivedUpdates = await updateCollector.waitForMessages(2000);
158
+
159
+ // Consumer should have received the joint updates from the state sync
160
+ expect(receivedUpdates.length).toBeGreaterThanOrEqual(1);
161
+ });
162
+
163
+ test("consumer receive joint updates", async () => {
164
+ const { workspaceId, roomId } = await producer.createRoom();
165
+ roomManager.addRoom(workspaceId, roomId);
166
+
167
+ const updateCollector = new MessageCollector(1);
168
+ consumer.onJointUpdate(updateCollector.collect);
169
+
170
+ await producer.connect(workspaceId, roomId);
171
+ await consumer.connect(workspaceId, roomId);
172
+
173
+ // Give some time for connection to stabilize
174
+ await sleep(100);
175
+
176
+ // Producer sends joint updates
177
+ const testJoints = [
178
+ { name: "shoulder", value: 45.0 },
179
+ { name: "elbow", value: -20.0 }
180
+ ];
181
+ await producer.sendJointUpdate(testJoints);
182
+
183
+ // Wait for message to be received
184
+ const receivedUpdates = await updateCollector.waitForMessages(2000);
185
+
186
+ // Consumer should have received the joint update
187
+ expect(receivedUpdates.length).toBeGreaterThanOrEqual(1);
188
+ if (receivedUpdates.length > 0) {
189
+ const receivedJoints = receivedUpdates[receivedUpdates.length - 1];
190
+ expect(Array.isArray(receivedJoints)).toBe(true);
191
+ expect(receivedJoints).toHaveLength(2);
192
+ }
193
+ });
194
+
195
+ test("consumer multiple updates", async () => {
196
+ const { workspaceId, roomId } = await producer.createRoom();
197
+ roomManager.addRoom(workspaceId, roomId);
198
+
199
+ const updateCollector = new MessageCollector(3);
200
+ consumer.onJointUpdate(updateCollector.collect);
201
+
202
+ await producer.connect(workspaceId, roomId);
203
+ await consumer.connect(workspaceId, roomId);
204
+
205
+ // Give some time for connection to stabilize
206
+ await sleep(100);
207
+
208
+ // Send multiple updates
209
+ for (let i = 0; i < 5; i++) {
210
+ await producer.sendStateSync({
211
+ joint1: i * 10,
212
+ joint2: i * -5
213
+ });
214
+ await sleep(50);
215
+ }
216
+
217
+ // Wait for all messages to be received
218
+ const receivedUpdates = await updateCollector.waitForMessages(3000);
219
+
220
+ // Should have received multiple updates
221
+ expect(receivedUpdates.length).toBeGreaterThanOrEqual(3);
222
+ });
223
+
224
+ test("consumer emergency stop", async () => {
225
+ const { workspaceId, roomId } = await producer.createRoom();
226
+ roomManager.addRoom(workspaceId, roomId);
227
+
228
+ const errorCollector = new MessageCollector<string>(1);
229
+ consumer.onError(errorCollector.collect);
230
+
231
+ await producer.connect(workspaceId, roomId);
232
+ await consumer.connect(workspaceId, roomId);
233
+
234
+ // Give some time for connection to stabilize
235
+ await sleep(100);
236
+
237
+ // Producer sends emergency stop
238
+ await producer.sendEmergencyStop("Test emergency stop");
239
+
240
+ // Wait for message to be received
241
+ const receivedErrors = await errorCollector.waitForMessages(2000);
242
+
243
+ // Consumer should have received emergency stop as error
244
+ expect(receivedErrors.length).toBeGreaterThanOrEqual(1);
245
+ if (receivedErrors.length > 0) {
246
+ expect(receivedErrors[receivedErrors.length - 1].toLowerCase()).toContain("emergency stop");
247
+ }
248
+ });
249
+
250
+ test("custom participant id", async () => {
251
+ const { workspaceId, roomId } = await producer.createRoom();
252
+ roomManager.addRoom(workspaceId, roomId);
253
+ const customId = "custom-consumer-456";
254
+
255
+ await consumer.connect(workspaceId, roomId, customId);
256
+
257
+ const info = consumer.getConnectionInfo();
258
+ expect(info.participant_id).toBe(customId);
259
+ });
260
+
261
+ test("get state without connection", async () => {
262
+ expect(consumer.isConnected()).toBe(false);
263
+
264
+ await expect(consumer.getStateSyncAsync())
265
+ .rejects.toThrow("Must be connected");
266
+ });
267
+
268
+ test("consumer reconnection", async () => {
269
+ const { workspaceId, roomId } = await producer.createRoom();
270
+ roomManager.addRoom(workspaceId, roomId);
271
+
272
+ // First connection
273
+ await consumer.connect(workspaceId, roomId);
274
+ expect(consumer.isConnected()).toBe(true);
275
+
276
+ await consumer.disconnect();
277
+ expect(consumer.isConnected()).toBe(false);
278
+
279
+ // Reconnect to same room
280
+ await consumer.connect(workspaceId, roomId);
281
+ expect(consumer.isConnected()).toBe(true);
282
+ expect(consumer.getConnectionInfo().room_id).toBe(roomId);
283
+ expect(consumer.getConnectionInfo().workspace_id).toBe(workspaceId);
284
+ });
285
+
286
+ test("consumer state after producer updates", async () => {
287
+ const { workspaceId, roomId } = await producer.createRoom();
288
+ roomManager.addRoom(workspaceId, roomId);
289
+
290
+ await producer.connect(workspaceId, roomId);
291
+ await consumer.connect(workspaceId, roomId);
292
+
293
+ // Give some time for connection to stabilize
294
+ await sleep(100);
295
+
296
+ // Producer sends some state updates
297
+ await producer.sendStateSync({
298
+ shoulder: 45.0,
299
+ elbow: -20.0,
300
+ wrist: 10.0
301
+ });
302
+
303
+ // Wait for state to propagate
304
+ await sleep(200);
305
+
306
+ // Consumer should be able to get updated state
307
+ const state = await consumer.getStateSyncAsync();
308
+ expect(typeof state).toBe("object");
309
+
310
+ // State should contain the joints we sent
311
+ const expectedJoints = new Set(["shoulder", "elbow", "wrist"]);
312
+ if (Object.keys(state).length > 0) { // Only check if state is not empty
313
+ expect(new Set(Object.keys(state))).toEqual(expectedJoints);
314
+ }
315
+ });
316
+ });
client/js/tests/factory.test.ts ADDED
@@ -0,0 +1,218 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Tests for Factory Functions - equivalent to Python's test_factory_functions.py
3
+ */
4
+ import { test, expect, describe, beforeEach, afterEach } from "bun:test";
5
+ import { robotics } from "../src/index";
6
+ import { TEST_SERVER_URL, TestRoomManager, MessageCollector, sleep } from "./setup";
7
+
8
+ const { RoboticsProducer, RoboticsConsumer, createClient, createProducerClient, createConsumerClient } = robotics;
9
+
10
+ describe("Factory Functions", () => {
11
+ let roomManager: TestRoomManager;
12
+
13
+ beforeEach(() => {
14
+ roomManager = new TestRoomManager();
15
+ });
16
+
17
+ afterEach(async () => {
18
+ // Cleanup will be handled by individual tests
19
+ });
20
+
21
+ test("create client producer", () => {
22
+ const client = createClient("producer");
23
+ expect(client).toBeInstanceOf(RoboticsProducer);
24
+ expect(client.isConnected()).toBe(false);
25
+ expect(client.getConnectionInfo().base_url).toBe("http://localhost:8000");
26
+ });
27
+
28
+ test("create client consumer", () => {
29
+ const client = createClient("consumer");
30
+ expect(client).toBeInstanceOf(RoboticsConsumer);
31
+ expect(client.isConnected()).toBe(false);
32
+ expect(client.getConnectionInfo().base_url).toBe("http://localhost:8000");
33
+ });
34
+
35
+ test("create client invalid role", () => {
36
+ expect(() => createClient("invalid_role" as any)).toThrow("Invalid role");
37
+ });
38
+
39
+ test("create client default url", () => {
40
+ const client = createClient("producer");
41
+ expect(client.getConnectionInfo().base_url).toBe("http://localhost:8000");
42
+ });
43
+
44
+ test("create producer client auto room", async () => {
45
+ const producer = await createProducerClient(TEST_SERVER_URL);
46
+
47
+ try {
48
+ expect(producer).toBeInstanceOf(RoboticsProducer);
49
+ expect(producer.isConnected()).toBe(true);
50
+ const info = producer.getConnectionInfo();
51
+ expect(info.room_id).toBeTruthy();
52
+ expect(info.workspace_id).toBeTruthy();
53
+ expect(info.role).toBe("producer");
54
+
55
+ // Should be able to send commands immediately
56
+ await producer.sendStateSync({ test: 123.0 });
57
+
58
+ // Track for cleanup
59
+ roomManager.addRoom(info.workspace_id!, info.room_id!);
60
+ } finally {
61
+ await producer.disconnect();
62
+ await roomManager.cleanup(producer);
63
+ }
64
+ });
65
+
66
+ test("create producer client specific room", async () => {
67
+ // First create a room
68
+ const tempProducer = new RoboticsProducer(TEST_SERVER_URL);
69
+ const { workspaceId, roomId } = await tempProducer.createRoom();
70
+ roomManager.addRoom(workspaceId, roomId);
71
+
72
+ try {
73
+ const producer = await createProducerClient(TEST_SERVER_URL, workspaceId, roomId);
74
+
75
+ expect(producer).toBeInstanceOf(RoboticsProducer);
76
+ expect(producer.isConnected()).toBe(true);
77
+ const info = producer.getConnectionInfo();
78
+ expect(info.room_id).toBe(roomId);
79
+ expect(info.workspace_id).toBe(workspaceId);
80
+ expect(info.role).toBe("producer");
81
+
82
+ await producer.disconnect();
83
+ } finally {
84
+ await roomManager.cleanup(tempProducer);
85
+ }
86
+ });
87
+
88
+ test("create consumer client", async () => {
89
+ // First create a room
90
+ const tempProducer = new RoboticsProducer(TEST_SERVER_URL);
91
+ const { workspaceId, roomId } = await tempProducer.createRoom();
92
+ roomManager.addRoom(workspaceId, roomId);
93
+
94
+ try {
95
+ const consumer = await createConsumerClient(workspaceId, roomId, TEST_SERVER_URL);
96
+
97
+ expect(consumer).toBeInstanceOf(RoboticsConsumer);
98
+ expect(consumer.isConnected()).toBe(true);
99
+ const info = consumer.getConnectionInfo();
100
+ expect(info.room_id).toBe(roomId);
101
+ expect(info.workspace_id).toBe(workspaceId);
102
+ expect(info.role).toBe("consumer");
103
+
104
+ // Should be able to get state immediately
105
+ const state = await consumer.getStateSyncAsync();
106
+ expect(typeof state).toBe("object");
107
+
108
+ await consumer.disconnect();
109
+ } finally {
110
+ await roomManager.cleanup(tempProducer);
111
+ }
112
+ });
113
+
114
+ test("create producer consumer pair", async () => {
115
+ const producer = await createProducerClient(TEST_SERVER_URL);
116
+ const producerInfo = producer.getConnectionInfo();
117
+ const workspaceId = producerInfo.workspace_id!;
118
+ const roomId = producerInfo.room_id!;
119
+ roomManager.addRoom(workspaceId, roomId);
120
+
121
+ try {
122
+ const consumer = await createConsumerClient(workspaceId, roomId, TEST_SERVER_URL);
123
+
124
+ // Both should be connected to same room
125
+ const consumerInfo = consumer.getConnectionInfo();
126
+ expect(producerInfo.room_id).toBe(consumerInfo.room_id);
127
+ expect(producerInfo.workspace_id).toBe(consumerInfo.workspace_id);
128
+ expect(producer.isConnected()).toBe(true);
129
+ expect(consumer.isConnected()).toBe(true);
130
+
131
+ // Test communication
132
+ const updateCollector = new MessageCollector(1);
133
+ consumer.onJointUpdate(updateCollector.collect);
134
+
135
+ // Give some time for connection to stabilize
136
+ await sleep(100);
137
+
138
+ // Send update from producer
139
+ await producer.sendStateSync({ test_joint: 42.0 });
140
+
141
+ // Wait for message
142
+ const receivedUpdates = await updateCollector.waitForMessages(2000);
143
+
144
+ // Consumer should have received update
145
+ expect(receivedUpdates.length).toBeGreaterThanOrEqual(1);
146
+
147
+ await consumer.disconnect();
148
+ } finally {
149
+ await producer.disconnect();
150
+ await roomManager.cleanup(producer);
151
+ }
152
+ });
153
+
154
+ test("convenience functions with default url", async () => {
155
+ const producer = await createProducerClient();
156
+ const producerInfo = producer.getConnectionInfo();
157
+ const workspaceId = producerInfo.workspace_id!;
158
+ const roomId = producerInfo.room_id!;
159
+ roomManager.addRoom(workspaceId, roomId);
160
+
161
+ try {
162
+ expect(producerInfo.base_url).toBe("http://localhost:8000");
163
+ expect(producer.isConnected()).toBe(true);
164
+
165
+ const consumer = await createConsumerClient(workspaceId, roomId);
166
+
167
+ try {
168
+ const consumerInfo = consumer.getConnectionInfo();
169
+ expect(consumerInfo.base_url).toBe("http://localhost:8000");
170
+ expect(consumer.isConnected()).toBe(true);
171
+ } finally {
172
+ await consumer.disconnect();
173
+ }
174
+ } finally {
175
+ await producer.disconnect();
176
+ await roomManager.cleanup(producer);
177
+ }
178
+ });
179
+
180
+ test("multiple convenience producers", async () => {
181
+ const producer1 = await createProducerClient(TEST_SERVER_URL);
182
+ const producer2 = await createProducerClient(TEST_SERVER_URL);
183
+
184
+ try {
185
+ const info1 = producer1.getConnectionInfo();
186
+ const info2 = producer2.getConnectionInfo();
187
+
188
+ // Both should be connected to different rooms
189
+ expect(info1.room_id).not.toBe(info2.room_id);
190
+ expect(producer1.isConnected()).toBe(true);
191
+ expect(producer2.isConnected()).toBe(true);
192
+
193
+ // Track for cleanup
194
+ roomManager.addRoom(info1.workspace_id!, info1.room_id!);
195
+ roomManager.addRoom(info2.workspace_id!, info2.room_id!);
196
+
197
+ // Both should work independently
198
+ await producer1.sendStateSync({ joint1: 10.0 });
199
+ await producer2.sendStateSync({ joint2: 20.0 });
200
+ } finally {
201
+ await producer1.disconnect();
202
+ await producer2.disconnect();
203
+ await roomManager.cleanup(producer1);
204
+ }
205
+ });
206
+
207
+ test("create consumer nonexistent room", async () => {
208
+ const fakeWorkspaceId = "00000000-0000-0000-0000-000000000000";
209
+ const fakeRoomId = "00000000-0000-0000-0000-000000000000";
210
+
211
+ const consumer = new RoboticsConsumer(TEST_SERVER_URL);
212
+ const success = await consumer.connect(fakeWorkspaceId, fakeRoomId);
213
+
214
+ // Should fail to connect to non-existent room
215
+ expect(success).toBe(false);
216
+ expect(consumer.isConnected()).toBe(false);
217
+ });
218
+ });
client/js/tests/integration.test.ts ADDED
@@ -0,0 +1,353 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Integration tests for producer-consumer interactions - equivalent to Python's test_integration.py
3
+ */
4
+ import { test, expect, describe, beforeEach, afterEach } from "bun:test";
5
+ import { robotics } from "../src/index";
6
+ import { TEST_SERVER_URL, TestRoomManager, MessageCollector, sleep } from "./setup";
7
+
8
+ const { RoboticsProducer, createConsumerClient, createProducerClient } = robotics;
9
+
10
+ describe("Integration", () => {
11
+ let roomManager: TestRoomManager;
12
+
13
+ beforeEach(() => {
14
+ roomManager = new TestRoomManager();
15
+ });
16
+
17
+ afterEach(async () => {
18
+ // Cleanup handled by individual tests
19
+ });
20
+
21
+ test("full producer consumer workflow", async () => {
22
+ // Create producer and room
23
+ const producer = await createProducerClient(TEST_SERVER_URL);
24
+ const producerInfo = producer.getConnectionInfo();
25
+ const workspaceId = producerInfo.workspace_id!;
26
+ const roomId = producerInfo.room_id!;
27
+ roomManager.addRoom(workspaceId, roomId);
28
+
29
+ try {
30
+ // Create consumer and connect to same room
31
+ const consumer = await createConsumerClient(workspaceId, roomId, TEST_SERVER_URL);
32
+
33
+ try {
34
+ // Set up consumer to collect messages
35
+ const stateCollector = new MessageCollector<Record<string, number>>(1);
36
+ const updateCollector = new MessageCollector(4);
37
+ const errorCollector = new MessageCollector<string>(1);
38
+
39
+ consumer.onStateSync(stateCollector.collect);
40
+ consumer.onJointUpdate(updateCollector.collect);
41
+ consumer.onError(errorCollector.collect);
42
+
43
+ // Wait for connections to stabilize
44
+ await sleep(200);
45
+
46
+ // Producer sends initial state
47
+ const initialState = { shoulder: 0.0, elbow: 0.0, wrist: 0.0 };
48
+ await producer.sendStateSync(initialState);
49
+ await sleep(100);
50
+
51
+ // Producer sends series of joint updates
52
+ const jointSequences = [
53
+ [{ name: "shoulder", value: 45.0 }],
54
+ [{ name: "elbow", value: -30.0 }],
55
+ [{ name: "wrist", value: 15.0 }],
56
+ [
57
+ { name: "shoulder", value: 90.0 },
58
+ { name: "elbow", value: -60.0 }
59
+ ]
60
+ ];
61
+
62
+ for (const joints of jointSequences) {
63
+ await producer.sendJointUpdate(joints);
64
+ await sleep(100);
65
+ }
66
+
67
+ // Wait for all messages to be received
68
+ const receivedUpdates = await updateCollector.waitForMessages(3000);
69
+
70
+ // Verify consumer received messages
71
+ expect(receivedUpdates.length).toBeGreaterThanOrEqual(4);
72
+
73
+ // Verify final state
74
+ const finalState = await consumer.getStateSyncAsync();
75
+ const expectedFinalState = { shoulder: 90.0, elbow: -60.0, wrist: 15.0 };
76
+ expect(finalState).toEqual(expectedFinalState);
77
+
78
+ } finally {
79
+ await consumer.disconnect();
80
+ }
81
+ } finally {
82
+ await producer.disconnect();
83
+ await roomManager.cleanup(producer);
84
+ }
85
+ });
86
+
87
+ test("multiple consumers same room", async () => {
88
+ const producer = await createProducerClient(TEST_SERVER_URL);
89
+ const producerInfo = producer.getConnectionInfo();
90
+ const workspaceId = producerInfo.workspace_id!;
91
+ const roomId = producerInfo.room_id!;
92
+ roomManager.addRoom(workspaceId, roomId);
93
+
94
+ try {
95
+ // Create multiple consumers
96
+ const consumer1 = await createConsumerClient(workspaceId, roomId, TEST_SERVER_URL);
97
+ const consumer2 = await createConsumerClient(workspaceId, roomId, TEST_SERVER_URL);
98
+
99
+ try {
100
+ // Set up message collection for both consumers
101
+ const consumer1Collector = new MessageCollector(1);
102
+ const consumer2Collector = new MessageCollector(1);
103
+
104
+ consumer1.onJointUpdate(consumer1Collector.collect);
105
+ consumer2.onJointUpdate(consumer2Collector.collect);
106
+
107
+ // Wait for connections
108
+ await sleep(200);
109
+
110
+ // Producer sends updates
111
+ const testJoints = [
112
+ { name: "joint1", value: 10.0 },
113
+ { name: "joint2", value: 20.0 }
114
+ ];
115
+ await producer.sendJointUpdate(testJoints);
116
+
117
+ // Wait for message propagation
118
+ const consumer1Updates = await consumer1Collector.waitForMessages(2000);
119
+ const consumer2Updates = await consumer2Collector.waitForMessages(2000);
120
+
121
+ // Both consumers should receive the same update
122
+ expect(consumer1Updates.length).toBeGreaterThanOrEqual(1);
123
+ expect(consumer2Updates.length).toBeGreaterThanOrEqual(1);
124
+
125
+ // Verify both received same data
126
+ if (consumer1Updates.length > 0 && consumer2Updates.length > 0) {
127
+ expect(consumer1Updates[consumer1Updates.length - 1]).toEqual(
128
+ consumer2Updates[consumer2Updates.length - 1]
129
+ );
130
+ }
131
+
132
+ } finally {
133
+ await consumer1.disconnect();
134
+ await consumer2.disconnect();
135
+ }
136
+ } finally {
137
+ await producer.disconnect();
138
+ await roomManager.cleanup(producer);
139
+ }
140
+ });
141
+
142
+ test("emergency stop propagation", async () => {
143
+ const producer = await createProducerClient(TEST_SERVER_URL);
144
+ const producerInfo = producer.getConnectionInfo();
145
+ const workspaceId = producerInfo.workspace_id!;
146
+ const roomId = producerInfo.room_id!;
147
+ roomManager.addRoom(workspaceId, roomId);
148
+
149
+ try {
150
+ // Create consumers
151
+ const consumer1 = await createConsumerClient(workspaceId, roomId, TEST_SERVER_URL);
152
+ const consumer2 = await createConsumerClient(workspaceId, roomId, TEST_SERVER_URL);
153
+
154
+ try {
155
+ // Set up error collection
156
+ const consumer1ErrorCollector = new MessageCollector<string>(1);
157
+ const consumer2ErrorCollector = new MessageCollector<string>(1);
158
+
159
+ consumer1.onError(consumer1ErrorCollector.collect);
160
+ consumer2.onError(consumer2ErrorCollector.collect);
161
+
162
+ // Wait for connections
163
+ await sleep(200);
164
+
165
+ // Producer sends emergency stop
166
+ await producer.sendEmergencyStop("Integration test emergency stop");
167
+
168
+ // Wait for message propagation
169
+ const consumer1Errors = await consumer1ErrorCollector.waitForMessages(2000);
170
+ const consumer2Errors = await consumer2ErrorCollector.waitForMessages(2000);
171
+
172
+ // Both consumers should receive emergency stop
173
+ expect(consumer1Errors.length).toBeGreaterThanOrEqual(1);
174
+ expect(consumer2Errors.length).toBeGreaterThanOrEqual(1);
175
+
176
+ // Verify error messages contain emergency stop info
177
+ if (consumer1Errors.length > 0) {
178
+ expect(consumer1Errors[consumer1Errors.length - 1].toLowerCase()).toContain("emergency stop");
179
+ }
180
+ if (consumer2Errors.length > 0) {
181
+ expect(consumer2Errors[consumer2Errors.length - 1].toLowerCase()).toContain("emergency stop");
182
+ }
183
+
184
+ } finally {
185
+ await consumer1.disconnect();
186
+ await consumer2.disconnect();
187
+ }
188
+ } finally {
189
+ await producer.disconnect();
190
+ await roomManager.cleanup(producer);
191
+ }
192
+ });
193
+
194
+ test("producer reconnection workflow", async () => {
195
+ // Create room first
196
+ const tempProducer = new RoboticsProducer(TEST_SERVER_URL);
197
+ const { workspaceId, roomId } = await tempProducer.createRoom();
198
+ roomManager.addRoom(workspaceId, roomId);
199
+
200
+ try {
201
+ // Create consumer first
202
+ const consumer = await createConsumerClient(workspaceId, roomId, TEST_SERVER_URL);
203
+
204
+ try {
205
+ const updateCollector = new MessageCollector(2);
206
+ consumer.onJointUpdate(updateCollector.collect);
207
+
208
+ // Create producer and connect
209
+ const producer = new RoboticsProducer(TEST_SERVER_URL);
210
+ await producer.connect(workspaceId, roomId);
211
+
212
+ // Send initial update
213
+ await producer.sendStateSync({ joint1: 10.0 });
214
+ await sleep(100);
215
+
216
+ // Disconnect producer
217
+ await producer.disconnect();
218
+
219
+ // Reconnect producer
220
+ await producer.connect(workspaceId, roomId);
221
+
222
+ // Send another update
223
+ await producer.sendStateSync({ joint1: 20.0 });
224
+ await sleep(200);
225
+
226
+ // Consumer should have received both updates
227
+ const receivedUpdates = await updateCollector.waitForMessages(3000);
228
+ expect(receivedUpdates.length).toBeGreaterThanOrEqual(2);
229
+
230
+ await producer.disconnect();
231
+
232
+ } finally {
233
+ await consumer.disconnect();
234
+ }
235
+ } finally {
236
+ await roomManager.cleanup(tempProducer);
237
+ }
238
+ });
239
+
240
+ test("consumer late join", async () => {
241
+ const producer = await createProducerClient(TEST_SERVER_URL);
242
+ const producerInfo = producer.getConnectionInfo();
243
+ const workspaceId = producerInfo.workspace_id!;
244
+ const roomId = producerInfo.room_id!;
245
+ roomManager.addRoom(workspaceId, roomId);
246
+
247
+ try {
248
+ // Producer sends some updates before consumer joins
249
+ await producer.sendStateSync({ joint1: 10.0, joint2: 20.0 });
250
+ await sleep(100);
251
+
252
+ await producer.sendJointUpdate([{ name: "joint3", value: 30.0 }]);
253
+ await sleep(100);
254
+
255
+ // Now consumer joins
256
+ const consumer = await createConsumerClient(workspaceId, roomId, TEST_SERVER_URL);
257
+
258
+ try {
259
+ // Consumer should be able to get current state
260
+ const currentState = await consumer.getStateSyncAsync();
261
+
262
+ // Should contain all previously sent updates
263
+ const expectedState = { joint1: 10.0, joint2: 20.0, joint3: 30.0 };
264
+ expect(currentState).toEqual(expectedState);
265
+
266
+ } finally {
267
+ await consumer.disconnect();
268
+ }
269
+ } finally {
270
+ await producer.disconnect();
271
+ await roomManager.cleanup(producer);
272
+ }
273
+ });
274
+
275
+ test("room cleanup on producer disconnect", async () => {
276
+ const producer = await createProducerClient(TEST_SERVER_URL);
277
+ const producerInfo = producer.getConnectionInfo();
278
+ const workspaceId = producerInfo.workspace_id!;
279
+ const roomId = producerInfo.room_id!;
280
+ roomManager.addRoom(workspaceId, roomId);
281
+
282
+ try {
283
+ const consumer = await createConsumerClient(workspaceId, roomId, TEST_SERVER_URL);
284
+
285
+ try {
286
+ // Send some state
287
+ await producer.sendStateSync({ joint1: 42.0 });
288
+ await sleep(100);
289
+
290
+ // Verify state exists
291
+ const stateBefore = await consumer.getStateSyncAsync();
292
+ expect(stateBefore).toEqual({ joint1: 42.0 });
293
+
294
+ // Producer disconnects
295
+ await producer.disconnect();
296
+ await sleep(100);
297
+
298
+ // State should still be accessible to consumer
299
+ const stateAfter = await consumer.getStateSyncAsync();
300
+ expect(stateAfter).toEqual({ joint1: 42.0 });
301
+
302
+ } finally {
303
+ await consumer.disconnect();
304
+ }
305
+ } finally {
306
+ // Room cleanup handled by roomManager since producer disconnected
307
+ }
308
+ });
309
+
310
+ test("high frequency updates", async () => {
311
+ const producer = await createProducerClient(TEST_SERVER_URL);
312
+ const producerInfo = producer.getConnectionInfo();
313
+ const workspaceId = producerInfo.workspace_id!;
314
+ const roomId = producerInfo.room_id!;
315
+ roomManager.addRoom(workspaceId, roomId);
316
+
317
+ try {
318
+ const consumer = await createConsumerClient(workspaceId, roomId, TEST_SERVER_URL);
319
+
320
+ try {
321
+ const updateCollector = new MessageCollector(5);
322
+ consumer.onJointUpdate(updateCollector.collect);
323
+
324
+ // Wait for connection
325
+ await sleep(100);
326
+
327
+ // Send rapid updates
328
+ for (let i = 0; i < 20; i++) {
329
+ await producer.sendStateSync({ joint1: i, timestamp: i });
330
+ await sleep(10); // 10ms intervals
331
+ }
332
+
333
+ // Wait for all messages
334
+ const receivedUpdates = await updateCollector.waitForMessages(5000);
335
+
336
+ // Should have received multiple updates
337
+ // (exact number may vary due to change detection)
338
+ expect(receivedUpdates.length).toBeGreaterThanOrEqual(5);
339
+
340
+ // Final state should reflect last update
341
+ const finalState = await consumer.getStateSyncAsync();
342
+ expect(finalState.joint1).toBe(19);
343
+ expect(finalState.timestamp).toBe(19);
344
+
345
+ } finally {
346
+ await consumer.disconnect();
347
+ }
348
+ } finally {
349
+ await producer.disconnect();
350
+ await roomManager.cleanup(producer);
351
+ }
352
+ });
353
+ });
client/js/tests/producer.test.ts ADDED
@@ -0,0 +1,217 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Tests for RoboticsProducer - equivalent to Python's test_producer.py
3
+ */
4
+ import { test, expect, describe, beforeEach, afterEach } from "bun:test";
5
+ import { robotics } from "../src/index";
6
+ import { TEST_SERVER_URL, TestRoomManager, MessageCollector, sleep, assertIsConnected, assertIsDisconnected } from "./setup";
7
+
8
+ const { RoboticsProducer } = robotics;
9
+
10
+ describe("RoboticsProducer", () => {
11
+ let producer: robotics.RoboticsProducer;
12
+ let roomManager: TestRoomManager;
13
+
14
+ beforeEach(() => {
15
+ producer = new RoboticsProducer(TEST_SERVER_URL);
16
+ roomManager = new TestRoomManager();
17
+ });
18
+
19
+ afterEach(async () => {
20
+ if (producer.isConnected()) {
21
+ await producer.disconnect();
22
+ }
23
+ await roomManager.cleanup(producer);
24
+ });
25
+
26
+ test("producer connection", async () => {
27
+ // Create room first
28
+ const { workspaceId, roomId } = await producer.createRoom();
29
+ roomManager.addRoom(workspaceId, roomId);
30
+
31
+ expect(producer.isConnected()).toBe(false);
32
+
33
+ const success = await producer.connect(workspaceId, roomId);
34
+ expect(success).toBe(true);
35
+ assertIsConnected(producer, workspaceId, roomId);
36
+
37
+ const info = producer.getConnectionInfo();
38
+ expect(info.role).toBe("producer");
39
+
40
+ await producer.disconnect();
41
+ assertIsDisconnected(producer);
42
+ });
43
+
44
+ test("producer connection info", async () => {
45
+ const { workspaceId, roomId } = await producer.createRoom();
46
+ roomManager.addRoom(workspaceId, roomId);
47
+ await producer.connect(workspaceId, roomId);
48
+
49
+ const info = producer.getConnectionInfo();
50
+ expect(info.connected).toBe(true);
51
+ expect(info.room_id).toBe(roomId);
52
+ expect(info.workspace_id).toBe(workspaceId);
53
+ expect(info.role).toBe("producer");
54
+ expect(info.participant_id).toBeTruthy();
55
+ expect(info.base_url).toBe(TEST_SERVER_URL);
56
+ });
57
+
58
+ test("send joint update", async () => {
59
+ const { workspaceId, roomId } = await producer.createRoom();
60
+ roomManager.addRoom(workspaceId, roomId);
61
+ await producer.connect(workspaceId, roomId);
62
+
63
+ const joints = [
64
+ { name: "shoulder", value: 45.0 },
65
+ { name: "elbow", value: -20.0 },
66
+ { name: "wrist", value: 10.0 }
67
+ ];
68
+
69
+ // Should not throw an exception
70
+ await producer.sendJointUpdate(joints);
71
+ });
72
+
73
+ test("send state sync", async () => {
74
+ const { workspaceId, roomId } = await producer.createRoom();
75
+ roomManager.addRoom(workspaceId, roomId);
76
+ await producer.connect(workspaceId, roomId);
77
+
78
+ const state = { shoulder: 45.0, elbow: -20.0, wrist: 10.0 };
79
+
80
+ // Should not throw an exception
81
+ await producer.sendStateSync(state);
82
+ });
83
+
84
+ test("send emergency stop", async () => {
85
+ const { workspaceId, roomId } = await producer.createRoom();
86
+ roomManager.addRoom(workspaceId, roomId);
87
+ await producer.connect(workspaceId, roomId);
88
+
89
+ // Should not throw an exception
90
+ await producer.sendEmergencyStop("Test emergency stop");
91
+ await producer.sendEmergencyStop(); // Default reason
92
+ });
93
+
94
+ test("producer callbacks", async () => {
95
+ const { workspaceId, roomId } = await producer.createRoom();
96
+ roomManager.addRoom(workspaceId, roomId);
97
+
98
+ let connectedCalled = false;
99
+ let disconnectedCalled = false;
100
+ let errorCalled = false;
101
+ let errorMessage: string | null = null;
102
+
103
+ producer.onConnected(() => {
104
+ connectedCalled = true;
105
+ });
106
+
107
+ producer.onDisconnected(() => {
108
+ disconnectedCalled = true;
109
+ });
110
+
111
+ producer.onError((error) => {
112
+ errorCalled = true;
113
+ errorMessage = error;
114
+ });
115
+
116
+ // Connect and disconnect
117
+ await producer.connect(workspaceId, roomId);
118
+ await sleep(100); // Give callbacks time to execute
119
+ expect(connectedCalled).toBe(true);
120
+
121
+ await producer.disconnect();
122
+ await sleep(100); // Give callbacks time to execute
123
+ expect(disconnectedCalled).toBe(true);
124
+ });
125
+
126
+ test("send without connection", async () => {
127
+ expect(producer.isConnected()).toBe(false);
128
+
129
+ await expect(producer.sendJointUpdate([{ name: "test", value: 0 }]))
130
+ .rejects.toThrow("Must be connected");
131
+
132
+ await expect(producer.sendStateSync({ test: 0 }))
133
+ .rejects.toThrow("Must be connected");
134
+
135
+ await expect(producer.sendEmergencyStop())
136
+ .rejects.toThrow("Must be connected");
137
+ });
138
+
139
+ test("multiple connections", async () => {
140
+ // Connect to first room
141
+ const { workspaceId: workspaceId1, roomId: roomId1 } = await producer.createRoom();
142
+ roomManager.addRoom(workspaceId1, roomId1);
143
+ await producer.connect(workspaceId1, roomId1);
144
+
145
+ expect(producer.getConnectionInfo().room_id).toBe(roomId1);
146
+ expect(producer.getConnectionInfo().workspace_id).toBe(workspaceId1);
147
+
148
+ // Create second room
149
+ const { workspaceId: workspaceId2, roomId: roomId2 } = await producer.createRoom();
150
+ roomManager.addRoom(workspaceId2, roomId2);
151
+
152
+ // Connect to second room (should disconnect from first)
153
+ await producer.connect(workspaceId2, roomId2);
154
+ expect(producer.getConnectionInfo().room_id).toBe(roomId2);
155
+ expect(producer.getConnectionInfo().workspace_id).toBe(workspaceId2);
156
+ expect(producer.isConnected()).toBe(true);
157
+ });
158
+
159
+ test("duplicate producer connection", async () => {
160
+ const { workspaceId, roomId } = await producer.createRoom();
161
+ roomManager.addRoom(workspaceId, roomId);
162
+
163
+ const producer2 = new RoboticsProducer(TEST_SERVER_URL);
164
+
165
+ try {
166
+ // First producer connects successfully
167
+ const success1 = await producer.connect(workspaceId, roomId);
168
+ expect(success1).toBe(true);
169
+
170
+ // Second producer should fail to connect as producer
171
+ const success2 = await producer2.connect(workspaceId, roomId);
172
+ expect(success2).toBe(false); // Should fail since room already has producer
173
+ } finally {
174
+ if (producer2.isConnected()) {
175
+ await producer2.disconnect();
176
+ }
177
+ }
178
+ });
179
+
180
+ test("custom participant id", async () => {
181
+ const { workspaceId, roomId } = await producer.createRoom();
182
+ roomManager.addRoom(workspaceId, roomId);
183
+ const customId = "custom-producer-123";
184
+
185
+ await producer.connect(workspaceId, roomId, customId);
186
+
187
+ const info = producer.getConnectionInfo();
188
+ expect(info.participant_id).toBe(customId);
189
+ });
190
+
191
+ test("large joint update", async () => {
192
+ const { workspaceId, roomId } = await producer.createRoom();
193
+ roomManager.addRoom(workspaceId, roomId);
194
+ await producer.connect(workspaceId, roomId);
195
+
196
+ // Create a large joint update
197
+ const joints = Array.from({ length: 100 }, (_, i) => ({
198
+ name: `joint_${i}`,
199
+ value: i
200
+ }));
201
+
202
+ // Should handle large updates without issue
203
+ await producer.sendJointUpdate(joints);
204
+ });
205
+
206
+ test("rapid updates", async () => {
207
+ const { workspaceId, roomId } = await producer.createRoom();
208
+ roomManager.addRoom(workspaceId, roomId);
209
+ await producer.connect(workspaceId, roomId);
210
+
211
+ // Send multiple rapid updates
212
+ for (let i = 0; i < 10; i++) {
213
+ await producer.sendStateSync({ joint1: i, joint2: i * 2 });
214
+ await sleep(10); // Small delay
215
+ }
216
+ });
217
+ });
client/js/tests/rest-api.test.ts ADDED
@@ -0,0 +1,156 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Tests for REST API functionality - equivalent to Python's test_rest_api.py
3
+ */
4
+ import { test, expect, describe, beforeEach, afterEach } from "bun:test";
5
+ import { robotics } from "../src/index";
6
+ import { TEST_SERVER_URL, TestRoomManager } from "./setup";
7
+
8
+ const { RoboticsProducer } = robotics;
9
+
10
+ describe("REST API", () => {
11
+ let producer: robotics.RoboticsProducer;
12
+ let roomManager: TestRoomManager;
13
+
14
+ beforeEach(() => {
15
+ producer = new RoboticsProducer(TEST_SERVER_URL);
16
+ roomManager = new TestRoomManager();
17
+ });
18
+
19
+ afterEach(async () => {
20
+ if (producer.isConnected()) {
21
+ await producer.disconnect();
22
+ }
23
+ await roomManager.cleanup(producer);
24
+ });
25
+
26
+ test("list rooms empty", async () => {
27
+ // Use a temporary workspace ID
28
+ const workspaceId = `test-workspace-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
29
+ const rooms = await producer.listRooms(workspaceId);
30
+ expect(Array.isArray(rooms)).toBe(true);
31
+ });
32
+
33
+ test("create room", async () => {
34
+ const { workspaceId, roomId } = await producer.createRoom();
35
+ roomManager.addRoom(workspaceId, roomId);
36
+
37
+ expect(typeof workspaceId).toBe("string");
38
+ expect(typeof roomId).toBe("string");
39
+ expect(workspaceId.length).toBeGreaterThan(0);
40
+ expect(roomId.length).toBeGreaterThan(0);
41
+ });
42
+
43
+ test("create room with id", async () => {
44
+ const customRoomId = "test-room-123";
45
+ const customWorkspaceId = "test-workspace-456";
46
+ const { workspaceId, roomId } = await producer.createRoom(customWorkspaceId, customRoomId);
47
+ roomManager.addRoom(workspaceId, roomId);
48
+
49
+ expect(workspaceId).toBe(customWorkspaceId);
50
+ expect(roomId).toBe(customRoomId);
51
+ });
52
+
53
+ test("list rooms with rooms", async () => {
54
+ // Create a test room
55
+ const { workspaceId, roomId } = await producer.createRoom();
56
+ roomManager.addRoom(workspaceId, roomId);
57
+
58
+ const rooms = await producer.listRooms(workspaceId);
59
+ expect(Array.isArray(rooms)).toBe(true);
60
+ expect(rooms.length).toBeGreaterThanOrEqual(1);
61
+
62
+ // Check if our room is in the list
63
+ const roomIds = rooms.map(room => room.id);
64
+ expect(roomIds).toContain(roomId);
65
+
66
+ // Verify room structure
67
+ const testRoom = rooms.find(room => room.id === roomId);
68
+ expect(testRoom).toBeDefined();
69
+ expect(testRoom!).toHaveProperty("participants");
70
+ expect(testRoom!).toHaveProperty("joints_count");
71
+ });
72
+
73
+ test("get room info", async () => {
74
+ const { workspaceId, roomId } = await producer.createRoom();
75
+ roomManager.addRoom(workspaceId, roomId);
76
+
77
+ const roomInfo = await producer.getRoomInfo(workspaceId, roomId);
78
+ expect(typeof roomInfo).toBe("object");
79
+ expect(roomInfo.id).toBe(roomId);
80
+ expect(roomInfo.workspace_id).toBe(workspaceId);
81
+ expect(roomInfo).toHaveProperty("participants");
82
+ expect(roomInfo).toHaveProperty("joints_count");
83
+ expect(roomInfo).toHaveProperty("has_producer");
84
+ expect(roomInfo).toHaveProperty("active_consumers");
85
+ });
86
+
87
+ test("get room state", async () => {
88
+ const { workspaceId, roomId } = await producer.createRoom();
89
+ roomManager.addRoom(workspaceId, roomId);
90
+
91
+ const roomState = await producer.getRoomState(workspaceId, roomId);
92
+ expect(typeof roomState).toBe("object");
93
+ expect(roomState).toHaveProperty("room_id");
94
+ expect(roomState).toHaveProperty("workspace_id");
95
+ expect(roomState).toHaveProperty("joints");
96
+ expect(roomState).toHaveProperty("participants");
97
+ expect(roomState).toHaveProperty("timestamp");
98
+ expect(roomState.room_id).toBe(roomId);
99
+ expect(roomState.workspace_id).toBe(workspaceId);
100
+ });
101
+
102
+ test("delete room", async () => {
103
+ const { workspaceId, roomId } = await producer.createRoom();
104
+
105
+ // Verify room exists
106
+ const roomsBefore = await producer.listRooms(workspaceId);
107
+ const roomIdsBefore = roomsBefore.map(room => room.id);
108
+ expect(roomIdsBefore).toContain(roomId);
109
+
110
+ // Delete room
111
+ const success = await producer.deleteRoom(workspaceId, roomId);
112
+ expect(success).toBe(true);
113
+
114
+ // Verify room is deleted
115
+ const roomsAfter = await producer.listRooms(workspaceId);
116
+ const roomIdsAfter = roomsAfter.map(room => room.id);
117
+ expect(roomIdsAfter).not.toContain(roomId);
118
+ });
119
+
120
+ test("delete nonexistent room", async () => {
121
+ const workspaceId = `test-workspace-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
122
+ const fakeRoomId = "nonexistent-room-id";
123
+ const success = await producer.deleteRoom(workspaceId, fakeRoomId);
124
+ expect(success).toBe(false);
125
+ });
126
+
127
+ test("get room info nonexistent", async () => {
128
+ const workspaceId = `test-workspace-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
129
+ const fakeRoomId = "nonexistent-room-id";
130
+
131
+ // This should raise an exception or return error info
132
+ try {
133
+ await producer.getRoomInfo(workspaceId, fakeRoomId);
134
+ // If no exception, this is unexpected for nonexistent room
135
+ expect(true).toBe(false); // Force test failure
136
+ } catch (error) {
137
+ // Expected behavior for nonexistent room
138
+ expect(error).toBeDefined();
139
+ }
140
+ });
141
+
142
+ test("get room state nonexistent", async () => {
143
+ const workspaceId = `test-workspace-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
144
+ const fakeRoomId = "nonexistent-room-id";
145
+
146
+ // This should raise an exception or return error info
147
+ try {
148
+ await producer.getRoomState(workspaceId, fakeRoomId);
149
+ // If no exception, this is unexpected for nonexistent room
150
+ expect(true).toBe(false); // Force test failure
151
+ } catch (error) {
152
+ // Expected behavior for nonexistent room
153
+ expect(error).toBeDefined();
154
+ }
155
+ });
156
+ });
client/js/tests/setup.ts ADDED
@@ -0,0 +1,130 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Test setup and utilities for RobotHub TransportServer JS Client Tests
3
+ * Equivalent to Python's conftest.py
4
+ */
5
+ import { expect } from "bun:test";
6
+
7
+ // Default server URL for tests
8
+ export const TEST_SERVER_URL = "http://localhost:8000";
9
+
10
+ // Test timeout configuration
11
+ export const TEST_TIMEOUT = 10000; // 10 seconds
12
+
13
+ // Helper to generate test IDs
14
+ export function generateTestId(prefix: string = "test"): string {
15
+ return `${prefix}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
16
+ }
17
+
18
+ // Helper to wait for async operations
19
+ export function sleep(ms: number): Promise<void> {
20
+ return new Promise(resolve => setTimeout(resolve, ms));
21
+ }
22
+
23
+ // Test workspace and room management
24
+ export class TestRoomManager {
25
+ private createdRooms: Array<{ workspaceId: string; roomId: string }> = [];
26
+
27
+ addRoom(workspaceId: string, roomId: string) {
28
+ this.createdRooms.push({ workspaceId, roomId });
29
+ }
30
+
31
+ async cleanup(client: any) {
32
+ for (const { workspaceId, roomId } of this.createdRooms) {
33
+ try {
34
+ await client.deleteRoom(workspaceId, roomId);
35
+ } catch (error) {
36
+ // Ignore cleanup errors
37
+ console.warn(`Failed to cleanup room ${roomId}:`, error);
38
+ }
39
+ }
40
+ this.createdRooms = [];
41
+ }
42
+ }
43
+
44
+ // Mock frame source for video tests
45
+ export async function mockFrameSource(): Promise<ArrayBuffer | null> {
46
+ // Create a simple test frame (RGBA data)
47
+ const width = 320;
48
+ const height = 240;
49
+ const size = width * height * 4; // RGBA
50
+ const buffer = new ArrayBuffer(size);
51
+ const view = new Uint8Array(buffer);
52
+
53
+ // Fill with test pattern
54
+ for (let i = 0; i < size; i += 4) {
55
+ view[i] = 255; // R
56
+ view[i + 1] = 0; // G
57
+ view[i + 2] = 0; // B
58
+ view[i + 3] = 255; // A
59
+ }
60
+
61
+ return buffer;
62
+ }
63
+
64
+ // Test assertion helpers
65
+ export function assertIsConnected(client: any, workspaceId: string, roomId: string) {
66
+ if (!client.isConnected()) {
67
+ throw new Error("Expected client to be connected");
68
+ }
69
+ const info = client.getConnectionInfo();
70
+ if (!info.connected) {
71
+ throw new Error("Expected connection info to show connected");
72
+ }
73
+ if (info.workspace_id !== workspaceId) {
74
+ throw new Error(`Expected workspace_id ${workspaceId}, got ${info.workspace_id}`);
75
+ }
76
+ if (info.room_id !== roomId) {
77
+ throw new Error(`Expected room_id ${roomId}, got ${info.room_id}`);
78
+ }
79
+ }
80
+
81
+ export function assertIsDisconnected(client: any) {
82
+ if (client.isConnected()) {
83
+ throw new Error("Expected client to be disconnected");
84
+ }
85
+ const info = client.getConnectionInfo();
86
+ if (info.connected) {
87
+ throw new Error("Expected connection info to show disconnected");
88
+ }
89
+ }
90
+
91
+ // Message collection helper for testing callbacks
92
+ export class MessageCollector<T = any> {
93
+ private messages: T[] = [];
94
+ private promise: Promise<T[]> | null = null;
95
+ private resolve: ((value: T[]) => void) | null = null;
96
+
97
+ constructor(private expectedCount: number = 1) {}
98
+
99
+ collect = (message: T) => {
100
+ this.messages.push(message);
101
+ if (this.messages.length >= this.expectedCount && this.resolve) {
102
+ this.resolve([...this.messages]);
103
+ }
104
+ };
105
+
106
+ async waitForMessages(timeoutMs: number = 5000): Promise<T[]> {
107
+ if (this.messages.length >= this.expectedCount) {
108
+ return [...this.messages];
109
+ }
110
+
111
+ this.promise = new Promise((resolve, reject) => {
112
+ this.resolve = resolve;
113
+ setTimeout(() => {
114
+ reject(new Error(`Timeout waiting for ${this.expectedCount} messages, got ${this.messages.length}`));
115
+ }, timeoutMs);
116
+ });
117
+
118
+ return this.promise;
119
+ }
120
+
121
+ getMessages(): T[] {
122
+ return [...this.messages];
123
+ }
124
+
125
+ clear() {
126
+ this.messages = [];
127
+ this.promise = null;
128
+ this.resolve = null;
129
+ }
130
+ }
client/js/tests/video-client.test.ts ADDED
@@ -0,0 +1,254 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Tests for Video Client - equivalent to Python's test_video_client.py
3
+ */
4
+ import { test, expect, describe, beforeEach, afterEach } from "bun:test";
5
+ import { video } from "../src/index";
6
+ import { TEST_SERVER_URL, TestRoomManager, mockFrameSource } from "./setup";
7
+
8
+ const { VideoProducer, VideoConsumer, createProducerClient, createConsumerClient } = video;
9
+
10
+ describe("Video Types", () => {
11
+ test("resolution creation", () => {
12
+ const resolution = { width: 1920, height: 1080 };
13
+ expect(resolution.width).toBe(1920);
14
+ expect(resolution.height).toBe(1080);
15
+ });
16
+
17
+ test("video config creation", () => {
18
+ const config = {
19
+ encoding: "vp8" as video.VideoEncoding,
20
+ resolution: { width: 640, height: 480 },
21
+ framerate: 30,
22
+ bitrate: 1000000
23
+ };
24
+ expect(config.encoding).toBe("vp8");
25
+ expect(config.resolution?.width).toBe(640);
26
+ expect(config.framerate).toBe(30);
27
+ });
28
+
29
+ test("participant role enum", () => {
30
+ const producerRole = "producer";
31
+ const consumerRole = "consumer";
32
+ expect(producerRole).toBe("producer");
33
+ expect(consumerRole).toBe("consumer");
34
+ });
35
+ });
36
+
37
+ describe("Video Core", () => {
38
+ let roomManager: TestRoomManager;
39
+
40
+ beforeEach(() => {
41
+ roomManager = new TestRoomManager();
42
+ });
43
+
44
+ afterEach(async () => {
45
+ await roomManager.cleanup(new VideoProducer(TEST_SERVER_URL));
46
+ });
47
+
48
+ test("video producer creation", () => {
49
+ const producer = new VideoProducer(TEST_SERVER_URL);
50
+ expect(producer.getConnectionInfo().base_url).toBe(TEST_SERVER_URL);
51
+ expect(producer.isConnected()).toBe(false);
52
+ expect(producer.getConnectionInfo().room_id).toBeNull();
53
+ });
54
+
55
+ test("video consumer creation", () => {
56
+ const consumer = new VideoConsumer(TEST_SERVER_URL);
57
+ expect(consumer.getConnectionInfo().base_url).toBe(TEST_SERVER_URL);
58
+ expect(consumer.isConnected()).toBe(false);
59
+ expect(consumer.getConnectionInfo().room_id).toBeNull();
60
+ });
61
+
62
+ test("producer room creation", async () => {
63
+ try {
64
+ const producer = new VideoProducer(TEST_SERVER_URL);
65
+ const { workspaceId, roomId } = await producer.createRoom();
66
+ roomManager.addRoom(workspaceId, roomId);
67
+
68
+ expect(typeof roomId).toBe("string");
69
+ expect(roomId.length).toBeGreaterThan(0);
70
+ console.log(`✅ Created room: ${roomId}`);
71
+ } catch (error) {
72
+ console.warn(`⚠️ Server not available: ${error}`);
73
+ // Skip test if server not available
74
+ }
75
+ }, { timeout: 5000 });
76
+
77
+ test("consumer list rooms", async () => {
78
+ try {
79
+ const consumer = new VideoConsumer(TEST_SERVER_URL);
80
+ const workspaceId = "test-workspace-id";
81
+ const rooms = await consumer.listRooms(workspaceId);
82
+ expect(Array.isArray(rooms)).toBe(true);
83
+ console.log(`✅ Listed ${rooms.length} rooms`);
84
+ } catch (error) {
85
+ console.warn(`⚠️ Server not available: ${error}`);
86
+ // Skip test if server not available
87
+ }
88
+ }, { timeout: 5000 });
89
+ });
90
+
91
+ describe("Video Client Integration", () => {
92
+ let roomManager: TestRoomManager;
93
+
94
+ beforeEach(() => {
95
+ roomManager = new TestRoomManager();
96
+ });
97
+
98
+ afterEach(async () => {
99
+ await roomManager.cleanup(new VideoProducer(TEST_SERVER_URL));
100
+ });
101
+
102
+ test("producer consumer setup", () => {
103
+ // Test producer setup
104
+ const producer = new VideoProducer(TEST_SERVER_URL);
105
+ expect(producer.getLocalStream()).toBeNull();
106
+
107
+ // Test consumer setup
108
+ const consumer = new VideoConsumer(TEST_SERVER_URL);
109
+ expect(consumer.getRemoteStream()).toBeNull();
110
+
111
+ console.log("✅ Producer/Consumer setup test passed");
112
+ });
113
+
114
+ test("custom stream setup validation", async () => {
115
+ const producer = new VideoProducer(TEST_SERVER_URL);
116
+
117
+ // This will fail because we're not connected, but it tests the setup
118
+ try {
119
+ // Simulate starting a custom stream when not connected
120
+ await producer.startCamera();
121
+ expect(true).toBe(false); // Should not reach here
122
+ } catch (error: any) {
123
+ expect(error.message).toContain("Must be connected");
124
+ console.log("✅ Custom stream setup validation passed");
125
+ }
126
+ });
127
+
128
+ test("factory functions", async () => {
129
+ // Test that factory functions create the right types
130
+ // (We can't actually connect without a server)
131
+
132
+ try {
133
+ await createProducerClient(TEST_SERVER_URL);
134
+ } catch (error) {
135
+ // Expected to fail without server
136
+ console.log("✅ Producer factory function exists");
137
+ }
138
+
139
+ try {
140
+ await createConsumerClient("test-workspace", "test-room", TEST_SERVER_URL);
141
+ } catch (error) {
142
+ // Expected to fail without server
143
+ console.log("✅ Consumer factory function exists");
144
+ }
145
+
146
+ console.log("✅ Factory functions test passed");
147
+ });
148
+
149
+ test("mock frame source", async () => {
150
+ const frameData = await mockFrameSource();
151
+ expect(frameData).not.toBeNull();
152
+ expect(frameData).toBeInstanceOf(ArrayBuffer);
153
+
154
+ if (frameData) {
155
+ expect(frameData.byteLength).toBe(320 * 240 * 4); // RGBA
156
+ console.log("✅ Mock frame source test passed");
157
+ }
158
+ });
159
+
160
+ test("video config types", () => {
161
+ const config: video.VideoConfig = {
162
+ encoding: "h264",
163
+ resolution: { width: 1280, height: 720 },
164
+ framerate: 60,
165
+ bitrate: 2000000,
166
+ quality: 90
167
+ };
168
+
169
+ expect(config.encoding).toBe("h264");
170
+ expect(config.resolution?.width).toBe(1280);
171
+ expect(config.resolution?.height).toBe(720);
172
+ expect(config.framerate).toBe(60);
173
+ expect(config.bitrate).toBe(2000000);
174
+ expect(config.quality).toBe(90);
175
+
176
+ console.log("✅ Video config types test passed");
177
+ });
178
+
179
+ test("recovery config types", () => {
180
+ const recoveryConfig: video.RecoveryConfig = {
181
+ frame_timeout_ms: 200,
182
+ max_frame_reuse_count: 5,
183
+ recovery_policy: "freeze_last_frame",
184
+ fallback_policy: "connection_info",
185
+ show_hold_indicators: true,
186
+ fade_intensity: 0.8
187
+ };
188
+
189
+ expect(recoveryConfig.frame_timeout_ms).toBe(200);
190
+ expect(recoveryConfig.recovery_policy).toBe("freeze_last_frame");
191
+ expect(recoveryConfig.fallback_policy).toBe("connection_info");
192
+
193
+ console.log("✅ Recovery config types test passed");
194
+ });
195
+
196
+ test("participant info types", () => {
197
+ const participantInfo: video.ParticipantInfo = {
198
+ producer: "producer-123",
199
+ consumers: ["consumer-1", "consumer-2"],
200
+ total: 3
201
+ };
202
+
203
+ expect(participantInfo.producer).toBe("producer-123");
204
+ expect(participantInfo.consumers).toHaveLength(2);
205
+ expect(participantInfo.total).toBe(3);
206
+
207
+ console.log("✅ Participant info types test passed");
208
+ });
209
+
210
+ test("room info types", () => {
211
+ const roomInfo: video.RoomInfo = {
212
+ id: "room-123",
213
+ workspace_id: "workspace-456",
214
+ participants: {
215
+ producer: "producer-1",
216
+ consumers: ["consumer-1"],
217
+ total: 2
218
+ },
219
+ frame_count: 1000,
220
+ config: {
221
+ encoding: "vp8",
222
+ resolution: { width: 640, height: 480 },
223
+ framerate: 30
224
+ },
225
+ has_producer: true,
226
+ active_consumers: 1
227
+ };
228
+
229
+ expect(roomInfo.id).toBe("room-123");
230
+ expect(roomInfo.workspace_id).toBe("workspace-456");
231
+ expect(roomInfo.has_producer).toBe(true);
232
+ expect(roomInfo.active_consumers).toBe(1);
233
+
234
+ console.log("✅ Room info types test passed");
235
+ });
236
+
237
+ test("stream stats types", () => {
238
+ const streamStats: video.StreamStats = {
239
+ stream_id: "stream-123",
240
+ duration_seconds: 120.5,
241
+ frame_count: 3615,
242
+ total_bytes: 1048576,
243
+ average_fps: 30.0,
244
+ average_bitrate: 1000000
245
+ };
246
+
247
+ expect(streamStats.stream_id).toBe("stream-123");
248
+ expect(streamStats.duration_seconds).toBe(120.5);
249
+ expect(streamStats.frame_count).toBe(3615);
250
+ expect(streamStats.average_fps).toBe(30.0);
251
+
252
+ console.log("✅ Stream stats types test passed");
253
+ });
254
+ });
client/python/.gitignore ADDED
@@ -0,0 +1,207 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[codz]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py.cover
50
+ .hypothesis/
51
+ .pytest_cache/
52
+ cover/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ .pybuilder/
76
+ target/
77
+
78
+ # Jupyter Notebook
79
+ .ipynb_checkpoints
80
+
81
+ # IPython
82
+ profile_default/
83
+ ipython_config.py
84
+
85
+ # pyenv
86
+ # For a library or package, you might want to ignore these files since the code is
87
+ # intended to run in multiple environments; otherwise, check them in:
88
+ # .python-version
89
+
90
+ # pipenv
91
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
93
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
94
+ # install all needed dependencies.
95
+ #Pipfile.lock
96
+
97
+ # UV
98
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
99
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
100
+ # commonly ignored for libraries.
101
+ #uv.lock
102
+
103
+ # poetry
104
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
105
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
106
+ # commonly ignored for libraries.
107
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
108
+ #poetry.lock
109
+ #poetry.toml
110
+
111
+ # pdm
112
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
113
+ # pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
114
+ # https://pdm-project.org/en/latest/usage/project/#working-with-version-control
115
+ #pdm.lock
116
+ #pdm.toml
117
+ .pdm-python
118
+ .pdm-build/
119
+
120
+ # pixi
121
+ # Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
122
+ #pixi.lock
123
+ # Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
124
+ # in the .venv directory. It is recommended not to include this directory in version control.
125
+ .pixi
126
+
127
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
128
+ __pypackages__/
129
+
130
+ # Celery stuff
131
+ celerybeat-schedule
132
+ celerybeat.pid
133
+
134
+ # SageMath parsed files
135
+ *.sage.py
136
+
137
+ # Environments
138
+ .env
139
+ .envrc
140
+ .venv
141
+ env/
142
+ venv/
143
+ ENV/
144
+ env.bak/
145
+ venv.bak/
146
+
147
+ # Spyder project settings
148
+ .spyderproject
149
+ .spyproject
150
+
151
+ # Rope project settings
152
+ .ropeproject
153
+
154
+ # mkdocs documentation
155
+ /site
156
+
157
+ # mypy
158
+ .mypy_cache/
159
+ .dmypy.json
160
+ dmypy.json
161
+
162
+ # Pyre type checker
163
+ .pyre/
164
+
165
+ # pytype static type analyzer
166
+ .pytype/
167
+
168
+ # Cython debug symbols
169
+ cython_debug/
170
+
171
+ # PyCharm
172
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
173
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
174
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
175
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
176
+ #.idea/
177
+
178
+ # Abstra
179
+ # Abstra is an AI-powered process automation framework.
180
+ # Ignore directories containing user credentials, local state, and settings.
181
+ # Learn more at https://abstra.io/docs
182
+ .abstra/
183
+
184
+ # Visual Studio Code
185
+ # Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
186
+ # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
187
+ # and can be added to the global gitignore or merged into this file. However, if you prefer,
188
+ # you could uncomment the following to ignore the entire vscode folder
189
+ # .vscode/
190
+
191
+ # Ruff stuff:
192
+ .ruff_cache/
193
+
194
+ # PyPI configuration file
195
+ .pypirc
196
+
197
+ # Cursor
198
+ # Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
199
+ # exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
200
+ # refer to https://docs.cursor.com/context/ignore-files
201
+ .cursorignore
202
+ .cursorindexingignore
203
+
204
+ # Marimo
205
+ marimo/_static/
206
+ marimo/_lsp/
207
+ __marimo__/
client/python/uv.lock ADDED
The diff for this file is too large to render. See raw diff
 
server/.gitignore ADDED
@@ -0,0 +1,207 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[codz]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py.cover
50
+ .hypothesis/
51
+ .pytest_cache/
52
+ cover/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ .pybuilder/
76
+ target/
77
+
78
+ # Jupyter Notebook
79
+ .ipynb_checkpoints
80
+
81
+ # IPython
82
+ profile_default/
83
+ ipython_config.py
84
+
85
+ # pyenv
86
+ # For a library or package, you might want to ignore these files since the code is
87
+ # intended to run in multiple environments; otherwise, check them in:
88
+ # .python-version
89
+
90
+ # pipenv
91
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
93
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
94
+ # install all needed dependencies.
95
+ #Pipfile.lock
96
+
97
+ # UV
98
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
99
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
100
+ # commonly ignored for libraries.
101
+ #uv.lock
102
+
103
+ # poetry
104
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
105
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
106
+ # commonly ignored for libraries.
107
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
108
+ #poetry.lock
109
+ #poetry.toml
110
+
111
+ # pdm
112
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
113
+ # pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
114
+ # https://pdm-project.org/en/latest/usage/project/#working-with-version-control
115
+ #pdm.lock
116
+ #pdm.toml
117
+ .pdm-python
118
+ .pdm-build/
119
+
120
+ # pixi
121
+ # Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
122
+ #pixi.lock
123
+ # Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
124
+ # in the .venv directory. It is recommended not to include this directory in version control.
125
+ .pixi
126
+
127
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
128
+ __pypackages__/
129
+
130
+ # Celery stuff
131
+ celerybeat-schedule
132
+ celerybeat.pid
133
+
134
+ # SageMath parsed files
135
+ *.sage.py
136
+
137
+ # Environments
138
+ .env
139
+ .envrc
140
+ .venv
141
+ env/
142
+ venv/
143
+ ENV/
144
+ env.bak/
145
+ venv.bak/
146
+
147
+ # Spyder project settings
148
+ .spyderproject
149
+ .spyproject
150
+
151
+ # Rope project settings
152
+ .ropeproject
153
+
154
+ # mkdocs documentation
155
+ /site
156
+
157
+ # mypy
158
+ .mypy_cache/
159
+ .dmypy.json
160
+ dmypy.json
161
+
162
+ # Pyre type checker
163
+ .pyre/
164
+
165
+ # pytype static type analyzer
166
+ .pytype/
167
+
168
+ # Cython debug symbols
169
+ cython_debug/
170
+
171
+ # PyCharm
172
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
173
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
174
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
175
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
176
+ #.idea/
177
+
178
+ # Abstra
179
+ # Abstra is an AI-powered process automation framework.
180
+ # Ignore directories containing user credentials, local state, and settings.
181
+ # Learn more at https://abstra.io/docs
182
+ .abstra/
183
+
184
+ # Visual Studio Code
185
+ # Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
186
+ # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
187
+ # and can be added to the global gitignore or merged into this file. However, if you prefer,
188
+ # you could uncomment the following to ignore the entire vscode folder
189
+ # .vscode/
190
+
191
+ # Ruff stuff:
192
+ .ruff_cache/
193
+
194
+ # PyPI configuration file
195
+ .pypirc
196
+
197
+ # Cursor
198
+ # Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
199
+ # exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
200
+ # refer to https://docs.cursor.com/context/ignore-files
201
+ .cursorignore
202
+ .cursorindexingignore
203
+
204
+ # Marimo
205
+ marimo/_static/
206
+ marimo/_lsp/
207
+ __marimo__/
server/launch_with_ui.py CHANGED
@@ -107,7 +107,7 @@ else:
107
 
108
  if __name__ == "__main__":
109
  port = int(os.getenv("PORT", 7860))
110
- host = os.getenv("HOST", "0.0.0.0")
111
 
112
  logger.info("🤖 Starting RobotHub TransportServer Combined Server...")
113
  logger.info(f" - API available at: http://{host}:{port}/api/")
 
107
 
108
  if __name__ == "__main__":
109
  port = int(os.getenv("PORT", 7860))
110
+ host = os.getenv("HOST", "localhost")
111
 
112
  logger.info("🤖 Starting RobotHub TransportServer Combined Server...")
113
  logger.info(f" - API available at: http://{host}:{port}/api/")
server/launch_without_ui.py ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ import os
3
+
4
+ import uvicorn
5
+
6
+ # Import the existing API
7
+
8
+ logging.basicConfig(
9
+ level=logging.INFO,
10
+ format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
11
+ handlers=[logging.StreamHandler(), logging.FileHandler("server.log")],
12
+ )
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+ if __name__ == "__main__":
17
+ port = int(os.getenv("PORT", 8000))
18
+ host = os.getenv("HOST", "localhost")
19
+
20
+ logger.info("🤖 Starting RobotHub TransportServer API Server (Hot Reload Mode)...")
21
+ logger.info(f" - API available at: http://{host}:{port}/")
22
+ logger.info(f" - API docs at: http://{host}:{port}/docs")
23
+ logger.info(" - Hot reload enabled for development")
24
+
25
+ print(
26
+ f"🤖 Starting RobotHub TransportServer API Server on {host}:{port} (Hot Reload)"
27
+ )
28
+
29
+ uvicorn.run(
30
+ "src.api:app",
31
+ host=host,
32
+ port=port,
33
+ reload=True,
34
+ log_level="info",
35
+ )
server/server.log CHANGED
@@ -2949,3 +2949,53 @@ RuntimeError: WebSocket is not connected. Need to call "accept" first.
2949
  2025-06-24 23:21:44,637 - src.robotics.core - INFO - Producer producer_1750800104635_8x1wksqls left room 2d5e7658-50e7-4353-984a-3f242071514c in workspace 66f07ebe-6ed2-474a-83b8-961f30d15134
2950
  2025-06-24 23:21:44,637 - src.robotics.core - INFO - Deleted room 2d5e7658-50e7-4353-984a-3f242071514c from workspace 66f07ebe-6ed2-474a-83b8-961f30d15134
2951
  2025-06-24 23:21:44,643 - src.video.core - INFO - Producer producer_1750800100711_nxb7n7u43 left video room 4995e7c2-a10c-4017-b49d-9bcb60468ad9 in workspace 72b50832-75e5-447c-b8b5-07837b54c0fc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2949
  2025-06-24 23:21:44,637 - src.robotics.core - INFO - Producer producer_1750800104635_8x1wksqls left room 2d5e7658-50e7-4353-984a-3f242071514c in workspace 66f07ebe-6ed2-474a-83b8-961f30d15134
2950
  2025-06-24 23:21:44,637 - src.robotics.core - INFO - Deleted room 2d5e7658-50e7-4353-984a-3f242071514c from workspace 66f07ebe-6ed2-474a-83b8-961f30d15134
2951
  2025-06-24 23:21:44,643 - src.video.core - INFO - Producer producer_1750800100711_nxb7n7u43 left video room 4995e7c2-a10c-4017-b49d-9bcb60468ad9 in workspace 72b50832-75e5-447c-b8b5-07837b54c0fc
2952
+ 2025-06-25 01:50:30,088 - __main__ - INFO - 🤖 Starting RobotHub TransportServer API Server (Hot Reload Mode)...
2953
+ 2025-06-25 01:50:30,088 - __main__ - INFO - - API available at: http://localhost:8000/
2954
+ 2025-06-25 01:50:30,088 - __main__ - INFO - - API docs at: http://localhost:8000/docs
2955
+ 2025-06-25 01:50:30,088 - __main__ - INFO - - Hot reload enabled for development
2956
+ 2025-06-25 01:50:57,314 - src.video.core - INFO - Created video room quick-example-1750809057304-front in workspace 095e87f9-6331-4c0a-b13c-ede63936ef78
2957
+ 2025-06-25 01:50:57,319 - src.robotics.core - INFO - Created room quick-example-1750809057304-joint-input in workspace 095e87f9-6331-4c0a-b13c-ede63936ef78
2958
+ 2025-06-25 01:50:57,321 - src.robotics.core - INFO - Created room quick-example-1750809057304-joint-output in workspace 095e87f9-6331-4c0a-b13c-ede63936ef78
2959
+ 2025-06-25 01:51:08,913 - watchfiles.main - INFO - 3 changes detected
2960
+ 2025-06-25 01:51:25,277 - src.video.core - INFO - Created video room quick-example-1750809085270-front in workspace f0e7d351-fb17-4bfa-8285-6d24f6887bbd
2961
+ 2025-06-25 01:51:25,282 - src.robotics.core - INFO - Created room quick-example-1750809085270-joint-input in workspace f0e7d351-fb17-4bfa-8285-6d24f6887bbd
2962
+ 2025-06-25 01:51:25,283 - src.robotics.core - INFO - Created room quick-example-1750809085270-joint-output in workspace f0e7d351-fb17-4bfa-8285-6d24f6887bbd
2963
+ 2025-06-25 01:52:33,163 - src.video.core - INFO - Created video room quick-example-1750809153152-front in workspace 117d5428-eca1-455f-a8c3-1e27c311213e
2964
+ 2025-06-25 01:52:33,168 - src.robotics.core - INFO - Created room quick-example-1750809153152-joint-input in workspace 117d5428-eca1-455f-a8c3-1e27c311213e
2965
+ 2025-06-25 01:52:33,169 - src.robotics.core - INFO - Created room quick-example-1750809153152-joint-output in workspace 117d5428-eca1-455f-a8c3-1e27c311213e
2966
+ 2025-06-25 01:52:42,075 - src.video.core - INFO - Consumer quick-example-1750809153152-front-consumer joined video room quick-example-1750809153152-front in workspace 117d5428-eca1-455f-a8c3-1e27c311213e
2967
+ 2025-06-25 01:52:42,581 - src.robotics.core - INFO - Consumer quick-example-1750809153152-joint-input-consumer joined room quick-example-1750809153152-joint-input in workspace 117d5428-eca1-455f-a8c3-1e27c311213e
2968
+ 2025-06-25 01:52:42,585 - src.robotics.core - INFO - Producer quick-example-1750809153152-joint-output-producer joined room quick-example-1750809153152-joint-output in workspace 117d5428-eca1-455f-a8c3-1e27c311213e
2969
+ 2025-06-25 01:52:44,608 - src.video.core - INFO - Consumer quick-example-1750809153152-front-consumer left video room quick-example-1750809153152-front in workspace 117d5428-eca1-455f-a8c3-1e27c311213e
2970
+ 2025-06-25 01:52:44,611 - src.robotics.core - INFO - Consumer quick-example-1750809153152-joint-input-consumer left room quick-example-1750809153152-joint-input in workspace 117d5428-eca1-455f-a8c3-1e27c311213e
2971
+ 2025-06-25 01:52:44,612 - src.robotics.core - INFO - Producer quick-example-1750809153152-joint-output-producer left room quick-example-1750809153152-joint-output in workspace 117d5428-eca1-455f-a8c3-1e27c311213e
2972
+ 2025-06-25 01:53:27,540 - src.video.core - INFO - Created video room quick-example-1750809207533-front in workspace 05a016a6-04f8-428e-8ca1-ecf1c01f8886
2973
+ 2025-06-25 01:53:27,543 - src.robotics.core - INFO - Created room quick-example-1750809207533-joint-input in workspace 05a016a6-04f8-428e-8ca1-ecf1c01f8886
2974
+ 2025-06-25 01:53:27,545 - src.robotics.core - INFO - Created room quick-example-1750809207533-joint-output in workspace 05a016a6-04f8-428e-8ca1-ecf1c01f8886
2975
+ 2025-06-25 01:53:29,118 - src.video.core - INFO - Consumer quick-example-1750809207533-front-consumer joined video room quick-example-1750809207533-front in workspace 05a016a6-04f8-428e-8ca1-ecf1c01f8886
2976
+ 2025-06-25 01:53:29,627 - src.robotics.core - INFO - Consumer quick-example-1750809207533-joint-input-consumer joined room quick-example-1750809207533-joint-input in workspace 05a016a6-04f8-428e-8ca1-ecf1c01f8886
2977
+ 2025-06-25 01:53:29,631 - src.robotics.core - INFO - Producer quick-example-1750809207533-joint-output-producer joined room quick-example-1750809207533-joint-output in workspace 05a016a6-04f8-428e-8ca1-ecf1c01f8886
2978
+ 2025-06-25 01:53:31,654 - src.video.core - INFO - Consumer quick-example-1750809207533-front-consumer left video room quick-example-1750809207533-front in workspace 05a016a6-04f8-428e-8ca1-ecf1c01f8886
2979
+ 2025-06-25 01:53:31,660 - src.robotics.core - INFO - Consumer quick-example-1750809207533-joint-input-consumer left room quick-example-1750809207533-joint-input in workspace 05a016a6-04f8-428e-8ca1-ecf1c01f8886
2980
+ 2025-06-25 01:53:31,661 - src.robotics.core - INFO - Producer quick-example-1750809207533-joint-output-producer left room quick-example-1750809207533-joint-output in workspace 05a016a6-04f8-428e-8ca1-ecf1c01f8886
2981
+ 2025-06-25 01:53:39,480 - src.video.core - INFO - Created video room example-session-1750809219478-front in workspace 756391fd-94b7-46dd-ad05-09def2b8c5e5
2982
+ 2025-06-25 01:53:39,481 - src.video.core - INFO - Created video room example-session-1750809219478-wrist in workspace 756391fd-94b7-46dd-ad05-09def2b8c5e5
2983
+ 2025-06-25 01:53:39,482 - src.robotics.core - INFO - Created room example-session-1750809219478-joint-input in workspace 756391fd-94b7-46dd-ad05-09def2b8c5e5
2984
+ 2025-06-25 01:53:39,483 - src.robotics.core - INFO - Created room example-session-1750809219478-joint-output in workspace 756391fd-94b7-46dd-ad05-09def2b8c5e5
2985
+ 2025-06-25 01:53:41,045 - src.video.core - INFO - Consumer example-session-1750809219478-front-consumer joined video room example-session-1750809219478-front in workspace 756391fd-94b7-46dd-ad05-09def2b8c5e5
2986
+ 2025-06-25 01:53:41,554 - src.video.core - INFO - Consumer example-session-1750809219478-wrist-consumer joined video room example-session-1750809219478-wrist in workspace 756391fd-94b7-46dd-ad05-09def2b8c5e5
2987
+ 2025-06-25 01:53:42,059 - src.robotics.core - INFO - Consumer example-session-1750809219478-joint-input-consumer joined room example-session-1750809219478-joint-input in workspace 756391fd-94b7-46dd-ad05-09def2b8c5e5
2988
+ 2025-06-25 01:53:42,063 - src.robotics.core - INFO - Producer example-session-1750809219478-joint-output-producer joined room example-session-1750809219478-joint-output in workspace 756391fd-94b7-46dd-ad05-09def2b8c5e5
2989
+ 2025-06-25 01:53:52,092 - src.video.core - INFO - Consumer example-session-1750809219478-front-consumer left video room example-session-1750809219478-front in workspace 756391fd-94b7-46dd-ad05-09def2b8c5e5
2990
+ 2025-06-25 01:53:52,094 - src.video.core - INFO - Consumer example-session-1750809219478-wrist-consumer left video room example-session-1750809219478-wrist in workspace 756391fd-94b7-46dd-ad05-09def2b8c5e5
2991
+ 2025-06-25 01:53:52,094 - src.robotics.core - INFO - Consumer example-session-1750809219478-joint-input-consumer left room example-session-1750809219478-joint-input in workspace 756391fd-94b7-46dd-ad05-09def2b8c5e5
2992
+ 2025-06-25 01:53:52,094 - src.robotics.core - INFO - Producer example-session-1750809219478-joint-output-producer left room example-session-1750809219478-joint-output in workspace 756391fd-94b7-46dd-ad05-09def2b8c5e5
2993
+ 2025-06-25 01:55:11,230 - src.video.core - INFO - Created video room my-robot-01-front in workspace 3a6e0b4d-21e1-4625-9af9-61bc8cabe831
2994
+ 2025-06-25 01:55:11,248 - src.robotics.core - INFO - Created room my-robot-01-joint-input in workspace 3a6e0b4d-21e1-4625-9af9-61bc8cabe831
2995
+ 2025-06-25 01:55:11,254 - src.robotics.core - INFO - Created room my-robot-01-joint-output in workspace 3a6e0b4d-21e1-4625-9af9-61bc8cabe831
2996
+ 2025-06-25 01:55:12,987 - src.video.core - INFO - Consumer my-robot-01-front-consumer joined video room my-robot-01-front in workspace 3a6e0b4d-21e1-4625-9af9-61bc8cabe831
2997
+ 2025-06-25 01:55:13,501 - src.robotics.core - INFO - Consumer my-robot-01-joint-input-consumer joined room my-robot-01-joint-input in workspace 3a6e0b4d-21e1-4625-9af9-61bc8cabe831
2998
+ 2025-06-25 01:55:13,504 - src.robotics.core - INFO - Producer my-robot-01-joint-output-producer joined room my-robot-01-joint-output in workspace 3a6e0b4d-21e1-4625-9af9-61bc8cabe831
2999
+ 2025-06-25 01:55:13,508 - src.video.core - INFO - Consumer my-robot-01-front-consumer left video room my-robot-01-front in workspace 3a6e0b4d-21e1-4625-9af9-61bc8cabe831
3000
+ 2025-06-25 01:55:13,509 - src.robotics.core - INFO - Consumer my-robot-01-joint-input-consumer left room my-robot-01-joint-input in workspace 3a6e0b4d-21e1-4625-9af9-61bc8cabe831
3001
+ 2025-06-25 01:55:13,509 - src.robotics.core - INFO - Producer my-robot-01-joint-output-producer left room my-robot-01-joint-output in workspace 3a6e0b4d-21e1-4625-9af9-61bc8cabe831