luckyrobots 0.1.72__py3-none-any.whl → 0.1.74__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. luckyrobots/__init__.py +5 -32
  2. luckyrobots/client.py +143 -463
  3. luckyrobots/config/robots.yaml +96 -48
  4. luckyrobots/engine/__init__.py +5 -20
  5. luckyrobots/grpc/generated/agent_pb2.py +4 -4
  6. luckyrobots/grpc/generated/agent_pb2_grpc.py +1 -1
  7. luckyrobots/grpc/generated/camera_pb2.py +2 -2
  8. luckyrobots/grpc/generated/camera_pb2_grpc.py +2 -2
  9. luckyrobots/grpc/generated/debug_pb2.py +51 -0
  10. luckyrobots/grpc/generated/debug_pb2_grpc.py +100 -0
  11. luckyrobots/grpc/generated/hazel_rpc_pb2.py +10 -9
  12. luckyrobots/grpc/generated/mujoco_pb2_grpc.py +1 -1
  13. luckyrobots/grpc/generated/scene_pb2.py +1 -1
  14. luckyrobots/grpc/generated/scene_pb2_grpc.py +1 -1
  15. luckyrobots/grpc/generated/telemetry_pb2_grpc.py +1 -1
  16. luckyrobots/grpc/generated/viewport_pb2.py +1 -1
  17. luckyrobots/grpc/generated/viewport_pb2_grpc.py +2 -2
  18. luckyrobots/grpc/proto/debug.proto +63 -0
  19. luckyrobots/grpc/proto/hazel_rpc.proto +1 -0
  20. luckyrobots/models/__init__.py +2 -14
  21. luckyrobots/models/observation.py +4 -33
  22. luckyrobots/utils.py +1 -43
  23. {luckyrobots-0.1.72.dist-info → luckyrobots-0.1.74.dist-info}/METADATA +1 -1
  24. luckyrobots-0.1.74.dist-info/RECORD +46 -0
  25. luckyrobots/engine/check_updates.py +0 -264
  26. luckyrobots/engine/download.py +0 -125
  27. luckyrobots/models/camera.py +0 -97
  28. luckyrobots/models/randomization.py +0 -77
  29. luckyrobots-0.1.72.dist-info/RECORD +0 -47
  30. {luckyrobots-0.1.72.dist-info → luckyrobots-0.1.74.dist-info}/WHEEL +0 -0
  31. {luckyrobots-0.1.72.dist-info → luckyrobots-0.1.74.dist-info}/licenses/LICENSE +0 -0
luckyrobots/client.py CHANGED
@@ -9,8 +9,6 @@ from __future__ import annotations
9
9
 
10
10
  import logging
11
11
  import time
12
- import statistics
13
- from dataclasses import dataclass
14
12
  from types import SimpleNamespace
15
13
  from typing import Any, Optional
16
14
 
@@ -19,26 +17,20 @@ logger = logging.getLogger("luckyrobots.client")
19
17
  try:
20
18
  from .grpc.generated import agent_pb2 # type: ignore
21
19
  from .grpc.generated import agent_pb2_grpc # type: ignore
22
- from .grpc.generated import camera_pb2 # type: ignore
23
- from .grpc.generated import camera_pb2_grpc # type: ignore
24
20
  from .grpc.generated import common_pb2 # type: ignore
25
- from .grpc.generated import media_pb2 # type: ignore
21
+ from .grpc.generated import debug_pb2 # type: ignore
22
+ from .grpc.generated import debug_pb2_grpc # type: ignore
26
23
  from .grpc.generated import mujoco_pb2 # type: ignore
27
24
  from .grpc.generated import mujoco_pb2_grpc # type: ignore
28
25
  from .grpc.generated import scene_pb2 # type: ignore
29
26
  from .grpc.generated import scene_pb2_grpc # type: ignore
30
- from .grpc.generated import telemetry_pb2 # type: ignore
31
- from .grpc.generated import telemetry_pb2_grpc # type: ignore
32
- from .grpc.generated import viewport_pb2 # type: ignore
33
- from .grpc.generated import viewport_pb2_grpc # type: ignore
34
27
  except Exception as e: # pragma: no cover
35
28
  raise ImportError(
36
29
  "Missing generated gRPC stubs. Regenerate them from the protos in "
37
30
  "src/luckyrobots/grpc/proto into src/luckyrobots/grpc/generated."
38
31
  ) from e
39
32
 
40
- # Import Pydantic models for type-checked responses
41
- from .models import ObservationResponse, StateSnapshot, DomainRandomizationConfig
33
+ from .models import ObservationResponse
42
34
 
43
35
 
44
36
  class GrpcConnectionError(Exception):
@@ -53,24 +45,18 @@ class LuckyEngineClient:
53
45
  """
54
46
  Client for connecting to the LuckyEngine gRPC server.
55
47
 
56
- Provides access to all gRPC services defined by the protos under
57
- `src/luckyrobots/rpc/proto`:
58
- - SceneService
59
- - MujocoService
60
- - TelemetryService
61
- - AgentService
62
- - ViewportService
63
- - CameraService
48
+ Provides access to gRPC services for RL training:
49
+ - AgentService: observations, stepping, resets
50
+ - SceneService: simulation mode control
51
+ - MujocoService: health checks
64
52
 
65
53
  Usage:
66
54
  client = LuckyEngineClient(host="127.0.0.1", port=50051)
67
55
  client.connect()
56
+ client.wait_for_server()
68
57
 
69
- # Access services
70
- scene_info = client.scene.GetSceneInfo(client.pb.scene.GetSceneInfoRequest())
71
- joint_state = client.mujoco.GetJointState(
72
- client.pb.mujoco.GetJointStateRequest()
73
- )
58
+ schema = client.get_agent_schema()
59
+ obs = client.step(actions=[0.0] * 12)
74
60
 
75
61
  client.close()
76
62
  """
@@ -102,25 +88,19 @@ class LuckyEngineClient:
102
88
  # Service stubs (populated after connect)
103
89
  self._scene = None
104
90
  self._mujoco = None
105
- self._telemetry = None
106
91
  self._agent = None
107
- self._viewport = None
108
- self._camera = None
92
+ self._debug = None
109
93
 
110
94
  # Cached agent schemas: agent_name -> (observation_names, action_names)
111
- # Populated lazily by get_agent_schema() or fetch_schema()
112
95
  self._schema_cache: dict[str, tuple[list[str], list[str]]] = {}
113
96
 
114
97
  # Protobuf modules (for discoverability + explicit imports).
115
98
  self._pb = SimpleNamespace(
116
99
  common=common_pb2,
117
- media=media_pb2,
118
100
  scene=scene_pb2,
119
101
  mujoco=mujoco_pb2,
120
- telemetry=telemetry_pb2,
121
102
  agent=agent_pb2,
122
- viewport=viewport_pb2,
123
- camera=camera_pb2,
103
+ debug=debug_pb2,
124
104
  )
125
105
 
126
106
  def connect(self) -> None:
@@ -145,10 +125,8 @@ class LuckyEngineClient:
145
125
  # Create service stubs
146
126
  self._scene = scene_pb2_grpc.SceneServiceStub(self._channel)
147
127
  self._mujoco = mujoco_pb2_grpc.MujocoServiceStub(self._channel)
148
- self._telemetry = telemetry_pb2_grpc.TelemetryServiceStub(self._channel)
149
128
  self._agent = agent_pb2_grpc.AgentServiceStub(self._channel)
150
- self._viewport = viewport_pb2_grpc.ViewportServiceStub(self._channel)
151
- self._camera = camera_pb2_grpc.CameraServiceStub(self._channel)
129
+ self._debug = debug_pb2_grpc.DebugServiceStub(self._channel)
152
130
 
153
131
  logger.info(f"Channel opened to {target} (server not verified yet)")
154
132
 
@@ -162,10 +140,8 @@ class LuckyEngineClient:
162
140
  self._channel = None
163
141
  self._scene = None
164
142
  self._mujoco = None
165
- self._telemetry = None
166
143
  self._agent = None
167
- self._viewport = None
168
- self._camera = None
144
+ self._debug = None
169
145
  logger.info("gRPC channel closed")
170
146
 
171
147
  def is_connected(self) -> bool:
@@ -209,8 +185,6 @@ class LuckyEngineClient:
209
185
  Returns:
210
186
  True if server became available, False if timeout.
211
187
  """
212
- import time
213
-
214
188
  start = time.perf_counter()
215
189
 
216
190
  while time.perf_counter() - start < timeout:
@@ -228,8 +202,6 @@ class LuckyEngineClient:
228
202
 
229
203
  return False
230
204
 
231
- # --- Protobuf modules (discoverable + explicit) ---
232
-
233
205
  @property
234
206
  def pb(self) -> Any:
235
207
  """Access protobuf modules grouped by domain (e.g., `client.pb.scene`)."""
@@ -244,21 +216,9 @@ class LuckyEngineClient:
244
216
  """Set the default robot name used by calls that accept an optional robot_name."""
245
217
  self._robot_name = robot_name
246
218
 
247
- # --- Service stubs ---
248
- #
249
- # Confirmed working:
250
- # - mujoco: GetMujocoInfo, GetJointState, SendControl
251
- # - agent: GetObservation, ResetAgent, GetAgentSchema, StreamAgent
252
- #
253
- # Placeholders (not yet confirmed working - use at your own risk):
254
- # - scene: GetSceneInfo
255
- # - telemetry: StreamTelemetry
256
- # - viewport: (no methods implemented)
257
- # - camera: ListCameras, StreamCamera
258
-
259
219
  @property
260
220
  def scene(self) -> Any:
261
- """SceneService stub. [PLACEHOLDER - not confirmed working]"""
221
+ """SceneService stub."""
262
222
  if self._scene is None:
263
223
  raise GrpcConnectionError("Not connected. Call connect() first.")
264
224
  return self._scene
@@ -270,13 +230,6 @@ class LuckyEngineClient:
270
230
  raise GrpcConnectionError("Not connected. Call connect() first.")
271
231
  return self._mujoco
272
232
 
273
- @property
274
- def telemetry(self) -> Any:
275
- """TelemetryService stub. [PLACEHOLDER - not confirmed working]"""
276
- if self._telemetry is None:
277
- raise GrpcConnectionError("Not connected. Call connect() first.")
278
- return self._telemetry
279
-
280
233
  @property
281
234
  def agent(self) -> Any:
282
235
  """AgentService stub."""
@@ -285,80 +238,23 @@ class LuckyEngineClient:
285
238
  return self._agent
286
239
 
287
240
  @property
288
- def viewport(self) -> Any:
289
- """ViewportService stub. [PLACEHOLDER - not confirmed working]"""
290
- if self._viewport is None:
291
- raise GrpcConnectionError("Not connected. Call connect() first.")
292
- return self._viewport
293
-
294
- @property
295
- def camera(self) -> Any:
296
- """CameraService stub. [PLACEHOLDER - not confirmed working]"""
297
- if self._camera is None:
241
+ def debug(self) -> Any:
242
+ """DebugService stub."""
243
+ if self._debug is None:
298
244
  raise GrpcConnectionError("Not connected. Call connect() first.")
299
- return self._camera
300
-
301
- # --- Convenience methods ---
302
-
303
- def get_scene_info(self, timeout: Optional[float] = None):
304
- """Get scene information. [PLACEHOLDER - not confirmed working]"""
305
- raise NotImplementedError(
306
- "get_scene_info() is not yet confirmed working. "
307
- "Remove this check if you want to test it."
308
- )
309
- timeout = timeout or self.timeout
310
- return self.scene.GetSceneInfo(
311
- self.pb.scene.GetSceneInfoRequest(),
312
- timeout=timeout,
313
- )
245
+ return self._debug
314
246
 
315
247
  def get_mujoco_info(self, robot_name: str = "", timeout: Optional[float] = None):
316
- """Get MuJoCo model information."""
248
+ """Get MuJoCo model information (joint names, limits, etc.)."""
317
249
  timeout = timeout or self.timeout
318
250
  robot_name = robot_name or self._robot_name
319
251
  if not robot_name:
320
- raise ValueError(
321
- "robot_name is required (pass `robot_name=` or set it once via "
322
- "LuckyEngineClient(robot_name=...) / client.set_robot_name(...))."
323
- )
252
+ raise ValueError("robot_name is required")
324
253
  return self.mujoco.GetMujocoInfo(
325
254
  self.pb.mujoco.GetMujocoInfoRequest(robot_name=robot_name),
326
255
  timeout=timeout,
327
256
  )
328
257
 
329
- def get_joint_state(self, robot_name: str = "", timeout: Optional[float] = None):
330
- """Get current joint state."""
331
- timeout = timeout or self.timeout
332
- robot_name = robot_name or self._robot_name
333
- if not robot_name:
334
- raise ValueError(
335
- "robot_name is required (pass `robot_name=` or set it once via "
336
- "LuckyEngineClient(robot_name=...) / client.set_robot_name(...))."
337
- )
338
- return self.mujoco.GetJointState(
339
- self.pb.mujoco.GetJointStateRequest(robot_name=robot_name),
340
- timeout=timeout,
341
- )
342
-
343
- def send_control(
344
- self,
345
- controls: list[float],
346
- robot_name: str = "",
347
- timeout: Optional[float] = None,
348
- ):
349
- """Send control commands to the robot."""
350
- timeout = timeout or self.timeout
351
- robot_name = robot_name or self._robot_name
352
- if not robot_name:
353
- raise ValueError(
354
- "robot_name is required (pass `robot_name=` or set it once via "
355
- "LuckyEngineClient(robot_name=...) / client.set_robot_name(...))."
356
- )
357
- return self.mujoco.SendControl(
358
- self.pb.mujoco.SendControlRequest(robot_name=robot_name, controls=controls),
359
- timeout=timeout,
360
- )
361
-
362
258
  def get_agent_schema(self, agent_name: str = "", timeout: Optional[float] = None):
363
259
  """Get agent schema (observation/action sizes and names).
364
260
 
@@ -395,18 +291,6 @@ class LuckyEngineClient:
395
291
 
396
292
  return resp
397
293
 
398
- def fetch_schema(self, agent_name: str = "", timeout: Optional[float] = None) -> None:
399
- """Fetch and cache agent schema for named observation access.
400
-
401
- Call this once before get_observation() to enable accessing observations
402
- by name (e.g., obs["proj_grav_x"]).
403
-
404
- Args:
405
- agent_name: Agent name (empty = default agent).
406
- timeout: RPC timeout.
407
- """
408
- self.get_agent_schema(agent_name=agent_name, timeout=timeout)
409
-
410
294
  def get_observation(
411
295
  self,
412
296
  agent_name: str = "",
@@ -415,10 +299,6 @@ class LuckyEngineClient:
415
299
  """
416
300
  Get the RL observation vector for an agent.
417
301
 
418
- This returns only the flat observation vector defined by the agent's
419
- observation spec in LuckyEngine. For sensor data (joints, telemetry,
420
- cameras), use the dedicated methods.
421
-
422
302
  Args:
423
303
  agent_name: Agent name (empty = default agent).
424
304
  timeout: RPC timeout.
@@ -435,30 +315,17 @@ class LuckyEngineClient:
435
315
  "LuckyEngineClient(robot_name=...) / client.set_robot_name(...))."
436
316
  )
437
317
 
438
- # Request only the agent's RL observation (agent_frame in gRPC terms).
439
- # Joint state and telemetry have their own dedicated methods.
440
318
  resp = self.agent.GetObservation(
441
319
  self.pb.agent.GetObservationRequest(
442
320
  robot_name=resolved_robot_name,
443
321
  agent_name=agent_name,
444
322
  include_joint_state=False,
445
- include_agent_frame=True, # The RL observation vector
323
+ include_agent_frame=True,
446
324
  include_telemetry=False,
447
325
  ),
448
326
  timeout=timeout,
449
327
  )
450
328
 
451
- # Extract observation from agent_frame.
452
- #
453
- # In gRPC, the "agent_frame" is the message containing the RL observation
454
- # data from LuckyEngine's agent system. It includes:
455
- # - observations: flat float vector matching the agent's observation spec
456
- # - actions: the last action vector sent to the agent (echoed back)
457
- # - timestamp_ms: wall-clock time when the observation was captured
458
- # - frame_number: monotonic counter for ordering observations
459
- #
460
- # This is distinct from joint_state (raw MuJoCo qpos/qvel) and telemetry
461
- # (debugging data like contact forces, energy, etc).
462
329
  agent_frame = getattr(resp, "agent_frame", None)
463
330
  observations = []
464
331
  actions = []
@@ -471,7 +338,6 @@ class LuckyEngineClient:
471
338
  timestamp_ms = getattr(agent_frame, "timestamp_ms", timestamp_ms)
472
339
  frame_number = getattr(agent_frame, "frame_number", frame_number)
473
340
 
474
- # Look up cached schema for named access
475
341
  cache_key = agent_name or "agent_0"
476
342
  obs_names, action_names = self._schema_cache.get(cache_key, (None, None))
477
343
 
@@ -485,117 +351,6 @@ class LuckyEngineClient:
485
351
  action_names=action_names,
486
352
  )
487
353
 
488
- def get_state(
489
- self,
490
- agent_name: str = "",
491
- include_observation: bool = True,
492
- include_joint_state: bool = True,
493
- camera_names: Optional[list[str]] = None,
494
- width: int = 0,
495
- height: int = 0,
496
- format: str = "raw",
497
- timeout: Optional[float] = None,
498
- ) -> StateSnapshot:
499
- """
500
- Get a bundled snapshot of multiple data sources.
501
-
502
- Use this for efficiency when you need multiple data types in one call.
503
- For single data types, prefer the dedicated methods:
504
- - get_observation() for RL observation vector
505
- - get_joint_state() for joint positions/velocities
506
- - stream_telemetry() for telemetry (streaming only)
507
- - stream_camera() for camera frames (streaming)
508
-
509
- Args:
510
- agent_name: Agent name (empty = default agent).
511
- include_observation: Include RL observation vector.
512
- include_joint_state: Include joint positions/velocities.
513
- camera_names: List of camera names to include frames from.
514
- width: Desired width for camera frames (0 = native).
515
- height: Desired height for camera frames (0 = native).
516
- format: Image format ("raw" or "jpeg").
517
- timeout: RPC timeout.
518
-
519
- Returns:
520
- StateSnapshot with requested data.
521
- """
522
- timeout = timeout or self.timeout
523
-
524
- resolved_robot_name = self._robot_name
525
- if not resolved_robot_name:
526
- raise ValueError(
527
- "robot_name is required (set it once via "
528
- "LuckyEngineClient(robot_name=...) / client.set_robot_name(...))."
529
- )
530
-
531
- cameras = []
532
- if camera_names:
533
- for name in camera_names:
534
- cameras.append(
535
- self.pb.agent.GetCameraFrameRequest(
536
- name=name,
537
- width=width,
538
- height=height,
539
- format=format,
540
- )
541
- )
542
-
543
- resp = self.agent.GetObservation(
544
- self.pb.agent.GetObservationRequest(
545
- robot_name=resolved_robot_name,
546
- agent_name=agent_name,
547
- include_joint_state=include_joint_state,
548
- include_agent_frame=include_observation,
549
- include_telemetry=False, # Telemetry is streaming-only
550
- cameras=cameras,
551
- ),
552
- timeout=timeout,
553
- )
554
-
555
- # Build ObservationResponse if requested
556
- obs_response = None
557
- if include_observation:
558
- agent_frame = getattr(resp, "agent_frame", None)
559
- observations = []
560
- actions = []
561
- if agent_frame is not None:
562
- observations = list(agent_frame.observations) if agent_frame.observations else []
563
- actions = list(agent_frame.actions) if agent_frame.actions else []
564
-
565
- # Look up cached schema for named access
566
- cache_key = agent_name or "agent_0"
567
- obs_names, action_names = self._schema_cache.get(cache_key, (None, None))
568
-
569
- obs_response = ObservationResponse(
570
- observation=observations,
571
- actions=actions,
572
- timestamp_ms=getattr(resp, "timestamp_ms", 0),
573
- frame_number=getattr(resp, "frame_number", 0),
574
- agent_name=cache_key,
575
- observation_names=obs_names,
576
- action_names=action_names,
577
- )
578
-
579
- return StateSnapshot(
580
- observation=obs_response,
581
- joint_state=getattr(resp, "joint_state", None) if include_joint_state else None,
582
- camera_frames=list(getattr(resp, "camera_frames", [])) if camera_names else None,
583
- timestamp_ms=getattr(resp, "timestamp_ms", 0),
584
- frame_number=getattr(resp, "frame_number", 0),
585
- )
586
-
587
- def stream_agent(self, agent_name: str = "", target_fps: int = 30):
588
- """
589
- Start streaming agent observations.
590
-
591
- Returns an iterator of AgentFrame messages.
592
- """
593
- return self.agent.StreamAgent(
594
- self.pb.agent.StreamAgentRequest(
595
- agent_name=agent_name, target_fps=target_fps
596
- ),
597
- )
598
-
599
354
  def reset_agent(
600
355
  self,
601
356
  agent_name: str = "",
@@ -603,13 +358,10 @@ class LuckyEngineClient:
603
358
  timeout: Optional[float] = None,
604
359
  ):
605
360
  """
606
- Reset a specific agent (full reset: clear buffers, reset state, resample commands, apply MuJoCo state).
607
-
608
- Useful for multi-env RL where individual agents need to be reset without resetting the entire scene.
361
+ Reset a specific agent.
609
362
 
610
363
  Args:
611
- agent_name: Agent logical name. Convention is `agent_0`, `agent_1`, ...
612
- Empty string means "default agent" (agent_0).
364
+ agent_name: Agent logical name. Empty string means default agent.
613
365
  randomization_cfg: Optional domain randomization config for this reset.
614
366
  timeout: Timeout in seconds (uses default if None).
615
367
 
@@ -618,7 +370,6 @@ class LuckyEngineClient:
618
370
  """
619
371
  timeout = timeout or self.timeout
620
372
 
621
- # Build request with optional randomization config
622
373
  request_kwargs = {"agent_name": agent_name}
623
374
 
624
375
  if randomization_cfg is not None:
@@ -640,13 +391,10 @@ class LuckyEngineClient:
640
391
  """
641
392
  Synchronous RL step: apply action, wait for physics, return observation.
642
393
 
643
- This is the recommended interface for RL training as it eliminates
644
- one network round-trip compared to separate SendControl + GetObservation.
645
-
646
394
  Args:
647
395
  actions: Action vector to apply for this step.
648
396
  agent_name: Agent name (empty = default agent).
649
- timeout_ms: Server-side timeout for the step in milliseconds (0 = use default).
397
+ timeout_ms: Server-side timeout for the step in milliseconds.
650
398
  timeout: RPC timeout in seconds.
651
399
 
652
400
  Returns:
@@ -666,14 +414,12 @@ class LuckyEngineClient:
666
414
  if not resp.success:
667
415
  raise RuntimeError(f"Step failed: {resp.message}")
668
416
 
669
- # Extract observation from AgentFrame
670
417
  agent_frame = resp.observation
671
418
  observations = list(agent_frame.observations) if agent_frame.observations else []
672
419
  actions_out = list(agent_frame.actions) if agent_frame.actions else []
673
420
  timestamp_ms = getattr(agent_frame, "timestamp_ms", 0)
674
421
  frame_number = getattr(agent_frame, "frame_number", 0)
675
422
 
676
- # Look up cached schema for named access
677
423
  cache_key = agent_name or "agent_0"
678
424
  obs_names, action_names = self._schema_cache.get(cache_key, (None, None))
679
425
 
@@ -697,8 +443,8 @@ class LuckyEngineClient:
697
443
 
698
444
  Args:
699
445
  mode: "realtime", "deterministic", or "fast"
700
- - realtime: Physics runs at 1x wall-clock speed (for visualization)
701
- - deterministic: Physics runs at fixed rate (for reproducibility)
446
+ - realtime: Physics runs at 1x wall-clock speed
447
+ - deterministic: Physics runs at fixed rate
702
448
  - fast: Physics runs as fast as possible (for RL training)
703
449
  timeout: RPC timeout in seconds.
704
450
 
@@ -712,39 +458,19 @@ class LuckyEngineClient:
712
458
  "deterministic": 1,
713
459
  "fast": 2,
714
460
  }
715
- mode_value = mode_map.get(mode.lower(), 2) # Default to fast
461
+ mode_value = mode_map.get(mode.lower(), 2)
716
462
 
717
463
  return self.scene.SetSimulationMode(
718
464
  self.pb.scene.SetSimulationModeRequest(mode=mode_value),
719
465
  timeout=timeout,
720
466
  )
721
467
 
722
- def get_simulation_mode(self, timeout: Optional[float] = None):
723
- """
724
- Get current simulation timing mode.
725
-
726
- Returns:
727
- GetSimulationModeResponse with mode field (0=realtime, 1=deterministic, 2=fast).
728
- """
729
- timeout = timeout or self.timeout
730
-
731
- return self.scene.GetSimulationMode(
732
- self.pb.scene.GetSimulationModeRequest(),
733
- timeout=timeout,
734
- )
735
-
736
468
  def _randomization_to_proto(self, randomization_cfg: Any):
737
- """Convert domain randomization config to proto message.
738
-
739
- Accepts any object with randomization config fields (DomainRandomizationConfig,
740
- PhysicsDRCfg from luckylab, or similar).
741
- """
469
+ """Convert domain randomization config to proto message."""
742
470
  proto_kwargs = {}
743
471
 
744
- # Helper to get attribute value, checking for None
745
472
  def get_val(name: str, default=None):
746
473
  val = getattr(randomization_cfg, name, default)
747
- # Handle both None and empty tuples/lists
748
474
  if val is None or (isinstance(val, (tuple, list)) and len(val) == 0):
749
475
  return None
750
476
  return val
@@ -766,7 +492,7 @@ class LuckyEngineClient:
766
492
  if joint_vel is not None and joint_vel != 0.0:
767
493
  proto_kwargs["joint_velocity_noise"] = joint_vel
768
494
 
769
- # Physics parameters (ranges)
495
+ # Physics parameters
770
496
  friction = get_val("friction_range")
771
497
  if friction is not None:
772
498
  proto_kwargs["friction_range"] = list(friction)
@@ -812,186 +538,140 @@ class LuckyEngineClient:
812
538
 
813
539
  return self.pb.agent.DomainRandomizationConfig(**proto_kwargs)
814
540
 
815
- def stream_telemetry(self, target_fps: int = 30):
541
+ def draw_velocity_command(
542
+ self,
543
+ origin: tuple[float, float, float],
544
+ lin_vel_x: float,
545
+ lin_vel_y: float,
546
+ ang_vel_z: float,
547
+ scale: float = 1.0,
548
+ clear_previous: bool = True,
549
+ timeout: Optional[float] = None,
550
+ ) -> bool:
816
551
  """
817
- Start streaming telemetry data. [PLACEHOLDER - not confirmed working]
552
+ Draw velocity command visualization in LuckyEngine.
818
553
 
819
- Returns an iterator of TelemetryFrame messages.
554
+ Args:
555
+ origin: (x, y, z) position of the robot.
556
+ lin_vel_x: Forward velocity command.
557
+ lin_vel_y: Lateral velocity command.
558
+ ang_vel_z: Angular velocity command (yaw rate).
559
+ scale: Scale factor for visualization.
560
+ clear_previous: Clear previous debug draws before drawing.
561
+ timeout: RPC timeout in seconds.
562
+
563
+ Returns:
564
+ True if draw succeeded, False otherwise.
820
565
  """
821
- raise NotImplementedError(
822
- "stream_telemetry() is not yet confirmed working. "
823
- "Remove this check if you want to test it."
824
- )
825
- return self.telemetry.StreamTelemetry(
826
- self.pb.telemetry.StreamTelemetryRequest(target_fps=target_fps),
827
- )
566
+ timeout = timeout or self.timeout
828
567
 
829
- def list_cameras(self, timeout: Optional[float] = None):
830
- """List available cameras. [PLACEHOLDER - not confirmed working]"""
831
- raise NotImplementedError(
832
- "list_cameras() is not yet confirmed working. "
833
- "Remove this check if you want to test it."
568
+ velocity_cmd = self.pb.debug.DebugVelocityCommand(
569
+ origin=self.pb.debug.DebugVector3(x=origin[0], y=origin[1], z=origin[2]),
570
+ lin_vel_x=lin_vel_x,
571
+ lin_vel_y=lin_vel_y,
572
+ ang_vel_z=ang_vel_z,
573
+ scale=scale,
834
574
  )
835
- timeout = timeout or self.timeout
836
- return self.camera.ListCameras(
837
- self.pb.camera.ListCamerasRequest(),
838
- timeout=timeout,
575
+
576
+ request = self.pb.debug.DebugDrawRequest(
577
+ velocity_command=velocity_cmd,
578
+ clear_previous=clear_previous,
839
579
  )
840
580
 
841
- def stream_camera(
581
+ try:
582
+ resp = self.debug.Draw(request, timeout=timeout)
583
+ return resp.success
584
+ except Exception as e:
585
+ logger.debug(f"Debug draw failed: {e}")
586
+ return False
587
+
588
+ def draw_arrow(
842
589
  self,
843
- camera_id: Optional[int] = None,
844
- camera_name: Optional[str] = None,
845
- target_fps: int = 30,
846
- width: int = 640,
847
- height: int = 480,
848
- format: str = "raw",
849
- ):
590
+ origin: tuple[float, float, float],
591
+ direction: tuple[float, float, float],
592
+ color: tuple[float, float, float, float] = (1.0, 0.0, 0.0, 1.0),
593
+ scale: float = 1.0,
594
+ clear_previous: bool = False,
595
+ timeout: Optional[float] = None,
596
+ ) -> bool:
850
597
  """
851
- Start streaming camera frames. [PLACEHOLDER - not confirmed working]
598
+ Draw a debug arrow in LuckyEngine.
852
599
 
853
600
  Args:
854
- camera_id: Camera entity ID (use either id or name).
855
- camera_name: Camera name (use either id or name).
856
- target_fps: Desired frames per second.
857
- width: Desired width (0 = native).
858
- height: Desired height (0 = native).
859
- format: Image format ("raw" or "jpeg").
860
-
861
- Returns an iterator of ImageFrame messages.
601
+ origin: (x, y, z) start position.
602
+ direction: (x, y, z) direction and magnitude.
603
+ color: (r, g, b, a) color values (0-1 range).
604
+ scale: Scale factor for visualization.
605
+ clear_previous: Clear previous debug draws before drawing.
606
+ timeout: RPC timeout in seconds.
607
+
608
+ Returns:
609
+ True if draw succeeded, False otherwise.
862
610
  """
863
- raise NotImplementedError(
864
- "stream_camera() is not yet confirmed working. "
865
- "Remove this check if you want to test it."
611
+ timeout = timeout or self.timeout
612
+
613
+ arrow = self.pb.debug.DebugArrow(
614
+ origin=self.pb.debug.DebugVector3(x=origin[0], y=origin[1], z=origin[2]),
615
+ direction=self.pb.debug.DebugVector3(
616
+ x=direction[0], y=direction[1], z=direction[2]
617
+ ),
618
+ color=self.pb.debug.DebugColor(
619
+ r=color[0], g=color[1], b=color[2], a=color[3]
620
+ ),
621
+ scale=scale,
866
622
  )
867
- if camera_id is not None:
868
- request = self.pb.camera.StreamCameraRequest(
869
- id=self.pb.common.EntityId(id=camera_id),
870
- target_fps=target_fps,
871
- width=width,
872
- height=height,
873
- format=format,
874
- )
875
- elif camera_name is not None:
876
- request = self.pb.camera.StreamCameraRequest(
877
- name=camera_name,
878
- target_fps=target_fps,
879
- width=width,
880
- height=height,
881
- format=format,
882
- )
883
- else:
884
- raise ValueError("Either camera_id or camera_name must be provided")
885
623
 
886
- return self.camera.StreamCamera(request)
624
+ request = self.pb.debug.DebugDrawRequest(
625
+ arrows=[arrow],
626
+ clear_previous=clear_previous,
627
+ )
887
628
 
888
- def benchmark(
629
+ try:
630
+ resp = self.debug.Draw(request, timeout=timeout)
631
+ return resp.success
632
+ except Exception as e:
633
+ logger.debug(f"Debug draw failed: {e}")
634
+ return False
635
+
636
+ def draw_line(
889
637
  self,
890
- duration_seconds: float = 5.0,
891
- method: str = "get_observation",
892
- print_results: bool = True,
893
- ) -> "BenchmarkResult":
638
+ start: tuple[float, float, float],
639
+ end: tuple[float, float, float],
640
+ color: tuple[float, float, float, float] = (1.0, 1.0, 1.0, 1.0),
641
+ clear_previous: bool = False,
642
+ timeout: Optional[float] = None,
643
+ ) -> bool:
894
644
  """
895
- Benchmark gRPC performance.
896
-
897
- Measures actual FPS and latency for observation calls.
645
+ Draw a debug line in LuckyEngine.
898
646
 
899
647
  Args:
900
- duration_seconds: How long to run the benchmark.
901
- method: Which method to benchmark ("get_observation" or "stream_agent").
902
- print_results: Whether to print results to console.
648
+ start: (x, y, z) start position.
649
+ end: (x, y, z) end position.
650
+ color: (r, g, b, a) color values (0-1 range).
651
+ clear_previous: Clear previous debug draws before drawing.
652
+ timeout: RPC timeout in seconds.
903
653
 
904
654
  Returns:
905
- BenchmarkResult with FPS and latency statistics.
655
+ True if draw succeeded, False otherwise.
906
656
  """
907
- if method not in ("get_observation", "stream_agent"):
908
- raise ValueError(f"Unknown method: {method}. Use 'get_observation' or 'stream_agent'.")
909
-
910
- latencies: list[float] = []
911
- frame_times: list[float] = []
912
- frame_count = 0
913
- start_time = time.perf_counter()
914
- last_frame_time = start_time
915
-
916
- if method == "get_observation":
917
- while time.perf_counter() - start_time < duration_seconds:
918
- call_start = time.perf_counter()
919
- self.get_observation()
920
- call_end = time.perf_counter()
921
-
922
- latencies.append((call_end - call_start) * 1000) # ms
923
- frame_times.append(call_end - last_frame_time)
924
- last_frame_time = call_end
925
- frame_count += 1
926
-
927
- elif method == "stream_agent":
928
- stream = self.stream_agent(target_fps=1000) # Request max FPS
929
- for frame in stream:
930
- now = time.perf_counter()
931
- if now - start_time >= duration_seconds:
932
- break
933
-
934
- frame_times.append(now - last_frame_time)
935
- last_frame_time = now
936
- frame_count += 1
937
-
938
- total_time = time.perf_counter() - start_time
939
-
940
- # Calculate statistics
941
- actual_fps = frame_count / total_time if total_time > 0 else 0
942
-
943
- if len(frame_times) > 1:
944
- # Remove first frame (warmup)
945
- frame_times = frame_times[1:]
946
- fps_from_frames = 1.0 / statistics.mean(frame_times) if frame_times else 0
947
- else:
948
- fps_from_frames = actual_fps
949
-
950
- result = BenchmarkResult(
951
- method=method,
952
- duration_seconds=total_time,
953
- frame_count=frame_count,
954
- actual_fps=actual_fps,
955
- avg_latency_ms=statistics.mean(latencies) if latencies else 0,
956
- min_latency_ms=min(latencies) if latencies else 0,
957
- max_latency_ms=max(latencies) if latencies else 0,
958
- std_latency_ms=statistics.stdev(latencies) if len(latencies) > 1 else 0,
959
- p50_latency_ms=statistics.median(latencies) if latencies else 0,
960
- p99_latency_ms=sorted(latencies)[int(len(latencies) * 0.99)] if latencies else 0,
657
+ timeout = timeout or self.timeout
658
+
659
+ line = self.pb.debug.DebugLine(
660
+ start=self.pb.debug.DebugVector3(x=start[0], y=start[1], z=start[2]),
661
+ end=self.pb.debug.DebugVector3(x=end[0], y=end[1], z=end[2]),
662
+ color=self.pb.debug.DebugColor(
663
+ r=color[0], g=color[1], b=color[2], a=color[3]
664
+ ),
961
665
  )
962
666
 
963
- if print_results:
964
- print(f"\n{'=' * 50}")
965
- print(f"Benchmark Results ({method})")
966
- print(f"{'=' * 50}")
967
- print(f"Duration: {result.duration_seconds:.2f}s")
968
- print(f"Frames: {result.frame_count}")
969
- print(f"Actual FPS: {result.actual_fps:.1f}")
970
- if latencies:
971
- print(f"{'─' * 50}")
972
- print(f"Latency (ms):")
973
- print(f" avg: {result.avg_latency_ms:.2f}")
974
- print(f" min: {result.min_latency_ms:.2f}")
975
- print(f" max: {result.max_latency_ms:.2f}")
976
- print(f" std: {result.std_latency_ms:.2f}")
977
- print(f" p50: {result.p50_latency_ms:.2f}")
978
- print(f" p99: {result.p99_latency_ms:.2f}")
979
- print(f"{'=' * 50}\n")
980
-
981
- return result
982
-
983
-
984
- @dataclass
985
- class BenchmarkResult:
986
- """Results from a benchmark run."""
987
-
988
- method: str
989
- duration_seconds: float
990
- frame_count: int
991
- actual_fps: float
992
- avg_latency_ms: float
993
- min_latency_ms: float
994
- max_latency_ms: float
995
- std_latency_ms: float
996
- p50_latency_ms: float
997
- p99_latency_ms: float
667
+ request = self.pb.debug.DebugDrawRequest(
668
+ lines=[line],
669
+ clear_previous=clear_previous,
670
+ )
671
+
672
+ try:
673
+ resp = self.debug.Draw(request, timeout=timeout)
674
+ return resp.success
675
+ except Exception as e:
676
+ logger.debug(f"Debug draw failed: {e}")
677
+ return False