luckyrobots 0.1.63__py3-none-any.whl → 0.1.72__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 (60) hide show
  1. luckyrobots/__init__.py +30 -12
  2. luckyrobots/client.py +997 -0
  3. luckyrobots/config/robots.yaml +231 -71
  4. luckyrobots/engine/__init__.py +23 -0
  5. luckyrobots/{utils → engine}/check_updates.py +108 -48
  6. luckyrobots/{utils → engine}/download.py +61 -39
  7. luckyrobots/engine/manager.py +427 -0
  8. luckyrobots/grpc/__init__.py +6 -0
  9. luckyrobots/grpc/generated/__init__.py +18 -0
  10. luckyrobots/grpc/generated/agent_pb2.py +69 -0
  11. luckyrobots/grpc/generated/agent_pb2_grpc.py +283 -0
  12. luckyrobots/grpc/generated/camera_pb2.py +47 -0
  13. luckyrobots/grpc/generated/camera_pb2_grpc.py +144 -0
  14. luckyrobots/grpc/generated/common_pb2.py +43 -0
  15. luckyrobots/grpc/generated/common_pb2_grpc.py +24 -0
  16. luckyrobots/grpc/generated/hazel_rpc_pb2.py +43 -0
  17. luckyrobots/grpc/generated/hazel_rpc_pb2_grpc.py +24 -0
  18. luckyrobots/grpc/generated/media_pb2.py +39 -0
  19. luckyrobots/grpc/generated/media_pb2_grpc.py +24 -0
  20. luckyrobots/grpc/generated/mujoco_pb2.py +51 -0
  21. luckyrobots/grpc/generated/mujoco_pb2_grpc.py +230 -0
  22. luckyrobots/grpc/generated/scene_pb2.py +66 -0
  23. luckyrobots/grpc/generated/scene_pb2_grpc.py +317 -0
  24. luckyrobots/grpc/generated/telemetry_pb2.py +47 -0
  25. luckyrobots/grpc/generated/telemetry_pb2_grpc.py +143 -0
  26. luckyrobots/grpc/generated/viewport_pb2.py +50 -0
  27. luckyrobots/grpc/generated/viewport_pb2_grpc.py +144 -0
  28. luckyrobots/grpc/proto/agent.proto +213 -0
  29. luckyrobots/grpc/proto/camera.proto +41 -0
  30. luckyrobots/grpc/proto/common.proto +36 -0
  31. luckyrobots/grpc/proto/hazel_rpc.proto +32 -0
  32. luckyrobots/grpc/proto/media.proto +26 -0
  33. luckyrobots/grpc/proto/mujoco.proto +64 -0
  34. luckyrobots/grpc/proto/scene.proto +104 -0
  35. luckyrobots/grpc/proto/telemetry.proto +43 -0
  36. luckyrobots/grpc/proto/viewport.proto +45 -0
  37. luckyrobots/luckyrobots.py +252 -0
  38. luckyrobots/models/__init__.py +15 -0
  39. luckyrobots/models/camera.py +97 -0
  40. luckyrobots/models/observation.py +135 -0
  41. luckyrobots/models/randomization.py +77 -0
  42. luckyrobots/{utils/helpers.py → utils.py} +75 -40
  43. luckyrobots-0.1.72.dist-info/METADATA +262 -0
  44. luckyrobots-0.1.72.dist-info/RECORD +47 -0
  45. {luckyrobots-0.1.63.dist-info → luckyrobots-0.1.72.dist-info}/WHEEL +1 -1
  46. luckyrobots/core/luckyrobots.py +0 -619
  47. luckyrobots/core/manager.py +0 -236
  48. luckyrobots/core/models.py +0 -68
  49. luckyrobots/core/node.py +0 -273
  50. luckyrobots/message/__init__.py +0 -18
  51. luckyrobots/message/pubsub.py +0 -145
  52. luckyrobots/message/srv/client.py +0 -81
  53. luckyrobots/message/srv/service.py +0 -135
  54. luckyrobots/message/srv/types.py +0 -83
  55. luckyrobots/message/transporter.py +0 -427
  56. luckyrobots/utils/event_loop.py +0 -94
  57. luckyrobots/utils/sim_manager.py +0 -406
  58. luckyrobots-0.1.63.dist-info/METADATA +0 -251
  59. luckyrobots-0.1.63.dist-info/RECORD +0 -24
  60. {luckyrobots-0.1.63.dist-info → luckyrobots-0.1.72.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,32 @@
1
+ syntax = "proto3";
2
+
3
+ // LuckyEngine / Hazel ScriptCore gRPC API (v1)
4
+ //
5
+ // This .proto is the *single source of truth* for the editor/runtime gRPC surface.
6
+ // Code generators produce language-specific stubs:
7
+ // - Client stubs: classes like `SceneServiceStub` / `AgentServiceStub` used by external tools
8
+ // - Server bindings: base/service descriptors used by ScriptCore to host implementations
9
+ //
10
+ // In this repo:
11
+ // - ScriptCore server host + impls live under `Hazel-ScriptCore/Source/Hazel/Net/Grpc/`
12
+ // - C# generated files are checked in under `Hazel-ScriptCore/Source/Hazel/Net/Grpc/Generated/`
13
+ // - A reference external Python client that generates Python stubs on-the-fly is `scripts/grpc_env.py`
14
+ //
15
+ // Compatibility note:
16
+ // - Avoid renumbering existing field tags (the `= N` numbers). Add new fields with new tags instead.
17
+
18
+ package hazel.rpc;
19
+
20
+ option cc_enable_arenas = true;
21
+
22
+ // This file is an umbrella entrypoint for the full API. The canonical message/service
23
+ // definitions live in smaller domain-specific protos for readability.
24
+
25
+ import "common.proto";
26
+ import "scene.proto";
27
+ import "mujoco.proto";
28
+ import "telemetry.proto";
29
+ import "media.proto";
30
+ import "agent.proto";
31
+ import "viewport.proto";
32
+ import "camera.proto";
@@ -0,0 +1,26 @@
1
+ syntax = "proto3";
2
+
3
+ // Media / image payload types shared across services.
4
+
5
+ package hazel.rpc;
6
+
7
+ option cc_enable_arenas = true;
8
+
9
+ // Generic image frame. For `format="raw"`, `data` is tightly-packed pixels (usually RGBA).
10
+ // For compressed formats, `data` is the encoded blob (e.g. JPEG bytes).
11
+ message ImageFrame {
12
+ bytes data = 1; // Raw pixel data (RGBA or compressed)
13
+ uint32 width = 2;
14
+ uint32 height = 3;
15
+ uint32 channels = 4; // 3 for RGB, 4 for RGBA
16
+ string format = 5; // "raw", "jpeg", "png"
17
+ uint64 timestamp_ms = 6; // Timestamp in milliseconds
18
+ uint32 frame_number = 7;
19
+ }
20
+
21
+ // Named image payload for observation snapshots.
22
+ // `name` can be a camera name or viewport name, depending on source.
23
+ message NamedImageFrame {
24
+ string name = 1;
25
+ ImageFrame frame = 2;
26
+ }
@@ -0,0 +1,64 @@
1
+ syntax = "proto3";
2
+
3
+ // MuJoCo service definitions for the LuckyEngine / Hazel ScriptCore gRPC API (v1).
4
+
5
+ package hazel.rpc;
6
+
7
+ option cc_enable_arenas = true;
8
+
9
+ // MuJoCo joint state.
10
+ message JointState {
11
+ repeated float positions = 1; // qpos
12
+ repeated float velocities = 2; // qvel
13
+ uint32 nq = 3; // Number of position coordinates
14
+ uint32 nv = 4; // Number of velocity coordinates
15
+ }
16
+
17
+ message GetJointStateRequest {
18
+ // Entity name or tag hint for which robot to target.
19
+ // Implementation may ignore this and use a "registered default robot" depending on current scene setup.
20
+ string robot_name = 1;
21
+ }
22
+
23
+ message GetJointStateResponse {
24
+ bool success = 1;
25
+ string message = 2;
26
+ JointState state = 3;
27
+ }
28
+
29
+ message SendControlRequest {
30
+ // Entity name or tag hint for which robot to target.
31
+ // Current Learn integration treats this as "set external actions for the next step".
32
+ string robot_name = 1;
33
+ repeated float controls = 2; // Control input vector
34
+ }
35
+
36
+ message SendControlResponse {
37
+ bool success = 1;
38
+ string message = 2;
39
+ }
40
+
41
+ message GetMujocoInfoRequest {
42
+ // Entity name or tag hint.
43
+ string robot_name = 1;
44
+ }
45
+
46
+ message GetMujocoInfoResponse {
47
+ bool success = 1;
48
+ string message = 2;
49
+ uint32 nq = 3;
50
+ uint32 nv = 4;
51
+ uint32 nu = 5; // Number of actuators
52
+ repeated string joint_names = 6;
53
+ repeated string actuator_names = 7;
54
+ }
55
+
56
+ // MuJoCo state + control IO.
57
+ service MujocoService {
58
+ rpc GetJointState(GetJointStateRequest) returns (GetJointStateResponse);
59
+ rpc SendControl(SendControlRequest) returns (SendControlResponse);
60
+ rpc GetMujocoInfo(GetMujocoInfoRequest) returns (GetMujocoInfoResponse);
61
+
62
+ // Streaming variant for continuous state observation
63
+ rpc StreamJointState(GetJointStateRequest) returns (stream GetJointStateResponse);
64
+ }
@@ -0,0 +1,104 @@
1
+ syntax = "proto3";
2
+
3
+ // Scene service definitions for the LuckyEngine / Hazel ScriptCore gRPC API (v1).
4
+
5
+ package hazel.rpc;
6
+
7
+ option cc_enable_arenas = true;
8
+
9
+ import "common.proto";
10
+
11
+ // Lightweight entity snapshot for editor tooling / inspection.
12
+ // Depending on request flags, `transform` and/or `components` may be left default/empty.
13
+ message EntityInfo {
14
+ EntityId id = 1;
15
+ string name = 2;
16
+ Transform transform = 3;
17
+ repeated string components = 4; // List of component type names (best-effort)
18
+ }
19
+
20
+ // Fetch basic active-scene metadata.
21
+ message GetSceneInfoRequest {}
22
+
23
+ message GetSceneInfoResponse {
24
+ string scene_name = 1;
25
+ string scene_path = 2;
26
+ uint32 entity_count = 3;
27
+ }
28
+
29
+ // List all entities, optionally including extra data to avoid unnecessary bandwidth.
30
+ message ListEntitiesRequest {
31
+ bool include_transforms = 1;
32
+ bool include_components = 2;
33
+ }
34
+
35
+ message ListEntitiesResponse {
36
+ repeated EntityInfo entities = 1;
37
+ }
38
+
39
+ // Fetch a single entity by ID or by name.
40
+ message GetEntityRequest {
41
+ oneof identifier {
42
+ EntityId id = 1;
43
+ string name = 2;
44
+ }
45
+ }
46
+
47
+ message GetEntityResponse {
48
+ bool found = 1;
49
+ EntityInfo entity = 2;
50
+ }
51
+
52
+ // Set an entity's transform.
53
+ // Note: server may clamp/ignore values depending on scene state.
54
+ message SetEntityTransformRequest {
55
+ EntityId id = 1;
56
+ Transform transform = 2;
57
+ }
58
+
59
+ message SetEntityTransformResponse {
60
+ bool success = 1;
61
+ string message = 2;
62
+ }
63
+
64
+ // =============================================================================
65
+ // Simulation Mode
66
+ // =============================================================================
67
+
68
+ // Simulation timing mode controls how physics stepping relates to wall-clock time.
69
+ enum SimulationMode {
70
+ // Real-time: physics runs at 1x wall-clock speed (for visualization, games).
71
+ SIMULATION_MODE_REALTIME = 0;
72
+ // Deterministic: physics runs at fixed rate regardless of wall-clock (for reproducibility).
73
+ SIMULATION_MODE_DETERMINISTIC = 1;
74
+ // Fast: physics runs as fast as possible without real-time limiting (for RL training).
75
+ SIMULATION_MODE_FAST = 2;
76
+ }
77
+
78
+ message SetSimulationModeRequest {
79
+ SimulationMode mode = 1;
80
+ }
81
+
82
+ message SetSimulationModeResponse {
83
+ bool success = 1;
84
+ string message = 2;
85
+ SimulationMode current_mode = 3;
86
+ }
87
+
88
+ message GetSimulationModeRequest {}
89
+
90
+ message GetSimulationModeResponse {
91
+ SimulationMode mode = 1;
92
+ }
93
+
94
+ // Scene inspection + basic editing.
95
+ service SceneService {
96
+ rpc GetSceneInfo(GetSceneInfoRequest) returns (GetSceneInfoResponse);
97
+ rpc ListEntities(ListEntitiesRequest) returns (ListEntitiesResponse);
98
+ rpc GetEntity(GetEntityRequest) returns (GetEntityResponse);
99
+ rpc SetEntityTransform(SetEntityTransformRequest) returns (SetEntityTransformResponse);
100
+ // Set simulation timing mode (realtime, deterministic, or fast).
101
+ rpc SetSimulationMode(SetSimulationModeRequest) returns (SetSimulationModeResponse);
102
+ // Get current simulation timing mode.
103
+ rpc GetSimulationMode(GetSimulationModeRequest) returns (GetSimulationModeResponse);
104
+ }
@@ -0,0 +1,43 @@
1
+ syntax = "proto3";
2
+
3
+ // Telemetry service definitions for the LuckyEngine / Hazel ScriptCore gRPC API (v1).
4
+
5
+ package hazel.rpc;
6
+
7
+ option cc_enable_arenas = true;
8
+
9
+ // Schema describing telemetry vectors (names are informational; sizes are authoritative).
10
+ message TelemetrySchema {
11
+ repeated string observation_names = 1;
12
+ repeated string action_names = 2;
13
+ uint32 nq = 3;
14
+ uint32 nu = 4;
15
+ }
16
+
17
+ message GetTelemetrySchemaRequest {}
18
+
19
+ message GetTelemetrySchemaResponse {
20
+ TelemetrySchema schema = 1;
21
+ }
22
+
23
+ // Request a server-stream of telemetry frames.
24
+ // Server may clamp `target_fps` based on configured limits.
25
+ message StreamTelemetryRequest {
26
+ uint32 target_fps = 1; // Desired sampling rate (frames per second)
27
+ }
28
+
29
+ // Telemetry frame: lightweight stream of simulator state (qpos) and last applied control.
30
+ message TelemetryFrame {
31
+ uint64 timestamp_ms = 1; // Wall-clock timestamp in milliseconds
32
+ uint32 frame_number = 2; // Monotonic frame counter
33
+ uint32 task_index = 3; // Reserved for future multi-task setups
34
+
35
+ repeated float observation_qpos = 4; // MuJoCo qpos[0..nq)
36
+ repeated float action_ctrl = 5; // MuJoCo ctrl[0..nu)
37
+ }
38
+
39
+ // Stream-only service intended for external tools (plotting, training logs, debugging).
40
+ service TelemetryService {
41
+ rpc GetTelemetrySchema(GetTelemetrySchemaRequest) returns (GetTelemetrySchemaResponse);
42
+ rpc StreamTelemetry(StreamTelemetryRequest) returns (stream TelemetryFrame);
43
+ }
@@ -0,0 +1,45 @@
1
+ syntax = "proto3";
2
+
3
+ // Viewport service definitions for the LuckyEngine / Hazel ScriptCore gRPC API (v1).
4
+
5
+ package hazel.rpc;
6
+
7
+ option cc_enable_arenas = true;
8
+
9
+ import "media.proto";
10
+
11
+ // Start a viewport stream. Server may clamp FPS and may choose an actual resolution.
12
+ message StartViewportStreamRequest {
13
+ string viewport_name = 1; // Which viewport to stream
14
+ uint32 target_fps = 2; // Desired frames per second
15
+ uint32 width = 3; // Desired width (0 = native)
16
+ uint32 height = 4; // Desired height (0 = native)
17
+ string format = 5; // "raw", "jpeg" (default: raw)
18
+ }
19
+
20
+ message ViewportStreamConfig {
21
+ bool streaming = 1;
22
+ string viewport_name = 2;
23
+ uint32 fps = 3;
24
+ uint32 width = 4;
25
+ uint32 height = 5;
26
+ }
27
+
28
+ message StopViewportStreamRequest {}
29
+
30
+ message StopViewportStreamResponse {
31
+ bool success = 1;
32
+ }
33
+
34
+ message GetViewportInfoRequest {}
35
+
36
+ message GetViewportInfoResponse {
37
+ repeated string available_viewports = 1;
38
+ ViewportStreamConfig current_config = 2;
39
+ }
40
+
41
+ // Streams pixels from a named viewport (useful for external visualization / training input).
42
+ service ViewportService {
43
+ rpc GetViewportInfo(GetViewportInfoRequest) returns (GetViewportInfoResponse);
44
+ rpc StreamViewport(StartViewportStreamRequest) returns (stream ImageFrame);
45
+ }
@@ -0,0 +1,252 @@
1
+ import logging
2
+ import time
3
+
4
+ from collections.abc import Sequence
5
+ from typing import Optional
6
+
7
+ from typing import Any
8
+
9
+ from .engine import launch_luckyengine, stop_luckyengine
10
+ from .models import ObservationResponse
11
+ from .client import LuckyEngineClient, GrpcConnectionError
12
+ from .utils import validate_params, get_robot_config
13
+
14
+ logger = logging.getLogger("luckyrobots")
15
+
16
+
17
+ class LuckyRobots:
18
+ """
19
+ gRPC-only control surface for LuckyEngine.
20
+
21
+ This is a small convenience wrapper around `LuckyEngineClient` that can:
22
+ - launch the LuckyEngine executable (optional)
23
+ - connect to the LuckyEngine gRPC server
24
+ - send controls and fetch unified observations
25
+ """
26
+
27
+ def __init__(
28
+ self,
29
+ host: str = "127.0.0.1",
30
+ port: int = 50051,
31
+ ) -> None:
32
+ self.host = host
33
+ self.port = port
34
+
35
+ self._engine_client: Optional[LuckyEngineClient] = None
36
+ self._robot_name: Optional[str] = None
37
+
38
+ # Cached metadata (filled after connect)
39
+ self._joint_names: Optional[list[str]] = None
40
+
41
+ @staticmethod
42
+ def get_robot_config(robot: str = None) -> dict:
43
+ """Return robot config from `luckyrobots/config/robots.yaml`."""
44
+ return get_robot_config(robot)
45
+
46
+ def start(
47
+ self,
48
+ scene: str,
49
+ robot: str,
50
+ task: str,
51
+ executable_path: str = None,
52
+ observation_type: str = "pixels_agent_pos",
53
+ headless: bool = False,
54
+ timeout_s: float = 120.0,
55
+ ) -> None:
56
+ """
57
+ Launch LuckyEngine (if needed) and connect to gRPC.
58
+
59
+ Args:
60
+ scene: LuckyEngine scene name.
61
+ robot: Robot name (must exist in `robots.yaml`).
62
+ task: Task name (must exist in `robots.yaml`).
63
+ executable_path: Path to LuckyEngine executable (optional; auto-detected).
64
+ observation_type: Used for validation and optional camera processing.
65
+ headless: Launch without rendering.
66
+ timeout_s: How long to wait for gRPC server to come up.
67
+ """
68
+ validate_params(scene, robot, task, observation_type)
69
+ self._robot_name = robot
70
+
71
+ success = launch_luckyengine(
72
+ scene=scene,
73
+ robot=robot,
74
+ task=task,
75
+ executable_path=executable_path,
76
+ headless=headless,
77
+ )
78
+ if not success:
79
+ logger.error("Failed to launch LuckyEngine")
80
+ raise RuntimeError(
81
+ "Failed to launch LuckyEngine. Look through the log for more information."
82
+ )
83
+
84
+ self.connect(timeout_s=timeout_s, robot=robot)
85
+
86
+ def connect(self, timeout_s: float = 120.0, robot: Optional[str] = None) -> None:
87
+ """Connect to LuckyEngine gRPC server and cache MuJoCo metadata."""
88
+ if robot is not None:
89
+ self._robot_name = robot
90
+ if not self._robot_name:
91
+ raise ValueError("Robot name is required (pass `robot=` or call start()).")
92
+
93
+ self._engine_client = LuckyEngineClient(
94
+ host=self.host,
95
+ port=self.port,
96
+ robot_name=self._robot_name,
97
+ )
98
+ logger.info(
99
+ "Waiting for LuckyEngine gRPC server at %s:%s", self.host, self.port
100
+ )
101
+ if not self._engine_client.wait_for_server(timeout=timeout_s):
102
+ raise GrpcConnectionError(
103
+ f"LuckyEngine gRPC server connection timeout after {timeout_s} seconds"
104
+ )
105
+
106
+ mujoco_info = self._engine_client.get_mujoco_info(robot_name=self._robot_name)
107
+ self._joint_names = (
108
+ list(mujoco_info.joint_names) if mujoco_info.joint_names else []
109
+ )
110
+ logger.info(
111
+ "Connected. MuJoCo: nq=%s nv=%s nu=%s joints=%s",
112
+ getattr(mujoco_info, "nq", None),
113
+ getattr(mujoco_info, "nv", None),
114
+ getattr(mujoco_info, "nu", None),
115
+ len(self._joint_names),
116
+ )
117
+
118
+ def _require_client(self) -> LuckyEngineClient:
119
+ if self._engine_client is None or not self._engine_client.is_connected():
120
+ raise GrpcConnectionError("Not connected. Call start() or connect() first.")
121
+ return self._engine_client
122
+
123
+ def get_observation(self, agent_name: str = "") -> ObservationResponse:
124
+ """
125
+ Get observation vector.
126
+
127
+ This returns only the flat observation vector defined by the agent's
128
+ observation spec. For sensor data, use the dedicated methods:
129
+ - get_joint_state() for joint positions/velocities
130
+ - engine_client.stream_telemetry() for telemetry
131
+ - engine_client.stream_camera() for camera frames
132
+
133
+ Args:
134
+ agent_name: Agent name (empty = default agent).
135
+
136
+ Returns:
137
+ ObservationResponse with observation vector, actions, timestamp.
138
+ """
139
+ client = self._require_client()
140
+ return client.get_observation(agent_name=agent_name)
141
+
142
+ def get_joint_state(self):
143
+ """
144
+ Get joint positions/velocities.
145
+
146
+ Returns the raw MuJoCo joint state for the robot.
147
+ """
148
+ client = self._require_client()
149
+ if not self._robot_name:
150
+ raise ValueError("Robot name is not set.")
151
+ return client.get_joint_state(robot_name=self._robot_name)
152
+
153
+ def send_control(self, controls: Sequence[float]) -> None:
154
+ """Send control commands to the robot via gRPC."""
155
+ client = self._require_client()
156
+ if not self._robot_name:
157
+ raise ValueError("Robot name is not set.")
158
+
159
+ resp = client.send_control(
160
+ controls=[float(x) for x in controls],
161
+ robot_name=self._robot_name,
162
+ )
163
+ if hasattr(resp, "success") and not resp.success:
164
+ raise RuntimeError(f"SendControl failed: {getattr(resp, 'message', '')}")
165
+
166
+ def step(
167
+ self,
168
+ actions: Sequence[float],
169
+ agent_name: str = "",
170
+ ) -> ObservationResponse:
171
+ """
172
+ Synchronous RL step: apply action, wait for physics, return observation.
173
+
174
+ This is the recommended interface for RL training. It uses the gRPC Step RPC
175
+ which combines SendControl + physics step + GetObservation into a single call,
176
+ eliminating one network round-trip.
177
+
178
+ Args:
179
+ actions: Action vector to apply for this step.
180
+ agent_name: Agent name (empty = default agent).
181
+
182
+ Returns:
183
+ ObservationResponse with observation after physics step.
184
+ """
185
+ client = self._require_client()
186
+ return client.step(actions=list(actions), agent_name=agent_name)
187
+
188
+ def set_simulation_mode(self, mode: str = "fast"):
189
+ """
190
+ Set simulation timing mode.
191
+
192
+ Args:
193
+ mode: "realtime", "deterministic", or "fast"
194
+ - realtime: Physics runs at 1x wall-clock speed (for visualization)
195
+ - deterministic: Physics runs at fixed rate (for reproducibility)
196
+ - fast: Physics runs as fast as possible (for RL training)
197
+ """
198
+ client = self._require_client()
199
+ return client.set_simulation_mode(mode=mode)
200
+
201
+ def get_simulation_mode(self):
202
+ """Get current simulation timing mode."""
203
+ client = self._require_client()
204
+ return client.get_simulation_mode()
205
+
206
+ def reset(
207
+ self,
208
+ agent_name: str = "",
209
+ randomization_cfg: Optional[Any] = None,
210
+ ) -> ObservationResponse:
211
+ """
212
+ Reset the agent and return a fresh observation.
213
+
214
+ Args:
215
+ agent_name: Agent logical name. Empty string means default agent.
216
+ randomization_cfg: Optional domain randomization config for this reset.
217
+ Use this to randomize physics parameters (friction, mass, etc.)
218
+ at the start of each episode for sim-to-real transfer.
219
+
220
+ Returns:
221
+ ObservationResponse after reset.
222
+
223
+ Raises:
224
+ RuntimeError: If reset fails.
225
+ """
226
+ client = self._require_client()
227
+ resp = client.reset_agent(agent_name=agent_name, randomization_cfg=randomization_cfg)
228
+ if hasattr(resp, "success") and not resp.success:
229
+ raise RuntimeError(f"Reset failed: {getattr(resp, 'message', '')}")
230
+ return self.get_observation(agent_name=agent_name)
231
+
232
+ def close(self, stop_engine: bool = True) -> None:
233
+ """Close gRPC client and optionally stop the engine executable."""
234
+ if self._engine_client is not None:
235
+ try:
236
+ self._engine_client.close()
237
+ finally:
238
+ self._engine_client = None
239
+
240
+ if stop_engine:
241
+ stop_luckyengine()
242
+
243
+ def __enter__(self) -> "LuckyRobots":
244
+ return self
245
+
246
+ def __exit__(self, exc_type, exc, tb) -> None:
247
+ self.close(stop_engine=True)
248
+
249
+ @property
250
+ def engine_client(self) -> Optional[LuckyEngineClient]:
251
+ """Access the underlying LuckyEngine gRPC client for advanced operations."""
252
+ return self._engine_client
@@ -0,0 +1,15 @@
1
+ """
2
+ Pydantic models for LuckyRobots.
3
+ """
4
+
5
+ from .observation import ObservationResponse, StateSnapshot
6
+ from .camera import CameraData, CameraShape
7
+ from .randomization import DomainRandomizationConfig
8
+
9
+ __all__ = [
10
+ "ObservationResponse",
11
+ "StateSnapshot",
12
+ "CameraData",
13
+ "CameraShape",
14
+ "DomainRandomizationConfig",
15
+ ]
@@ -0,0 +1,97 @@
1
+ """
2
+ Camera models for LuckyRobots.
3
+
4
+ These models handle camera frame data from LuckyEngine.
5
+ """
6
+
7
+ from typing import Any, Dict, Optional, Union
8
+ from pydantic import BaseModel, Field, ConfigDict
9
+ import numpy as np
10
+ import cv2
11
+
12
+
13
+ class CameraShape(BaseModel):
14
+ """Shape of camera images."""
15
+
16
+ width: float = Field(description="Width of the image")
17
+ height: float = Field(description="Height of the image")
18
+ channel: int = Field(description="Number of color channels")
19
+
20
+
21
+ class CameraData(BaseModel):
22
+ """Camera frame data."""
23
+
24
+ model_config = ConfigDict(populate_by_name=True, arbitrary_types_allowed=True)
25
+
26
+ camera_name: str = Field(alias="CameraName", description="Name of the camera")
27
+ dtype: str = Field(default="uint8", description="Data type of the image")
28
+ shape: Optional[Union[CameraShape, Dict[str, Union[float, int]]]] = Field(
29
+ default=None, description="Shape of the image"
30
+ )
31
+ time_stamp: Optional[str] = Field(
32
+ None, alias="TimeStamp", description="Camera timestamp"
33
+ )
34
+ image_data: Optional[Any] = Field(
35
+ None, alias="ImageData", description="Image data (bytes or numpy array)"
36
+ )
37
+ width: Optional[int] = Field(default=None, description="Image width")
38
+ height: Optional[int] = Field(default=None, description="Image height")
39
+ channels: Optional[int] = Field(default=None, description="Number of channels")
40
+ format: Optional[str] = Field(
41
+ default=None, description="Image format (raw, jpeg, png)"
42
+ )
43
+ frame_number: Optional[int] = Field(default=None, description="Frame number")
44
+
45
+ def process_image(self) -> None:
46
+ """Process the image data into a numpy array."""
47
+ if self.image_data is None:
48
+ return None
49
+
50
+ # If already a numpy array, skip processing
51
+ if isinstance(self.image_data, np.ndarray):
52
+ return
53
+
54
+ # Handle bytes data
55
+ if isinstance(self.image_data, bytes):
56
+ nparr = np.frombuffer(self.image_data, np.uint8)
57
+
58
+ # Check if it's raw RGBA/RGB data or encoded
59
+ if self.format == "raw" and self.width and self.height:
60
+ channels = self.channels or 4
61
+ try:
62
+ self.image_data = nparr.reshape((self.height, self.width, channels))
63
+ # Convert RGBA to BGR for OpenCV compatibility
64
+ if channels == 4:
65
+ self.image_data = cv2.cvtColor(
66
+ self.image_data, cv2.COLOR_RGBA2BGR
67
+ )
68
+ elif channels == 3:
69
+ self.image_data = cv2.cvtColor(
70
+ self.image_data, cv2.COLOR_RGB2BGR
71
+ )
72
+ except ValueError:
73
+ # Fallback to decode
74
+ self.image_data = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
75
+ else:
76
+ # Encoded image (JPEG, PNG)
77
+ self.image_data = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
78
+
79
+ @classmethod
80
+ def from_grpc_frame(cls, frame: Any, camera_name: str = "camera") -> "CameraData":
81
+ """Create CameraData from a gRPC ImageFrame message."""
82
+ return cls(
83
+ camera_name=camera_name,
84
+ dtype="uint8",
85
+ width=frame.width,
86
+ height=frame.height,
87
+ channels=frame.channels,
88
+ format=frame.format,
89
+ time_stamp=str(frame.timestamp_ms),
90
+ frame_number=frame.frame_number,
91
+ image_data=frame.data,
92
+ shape=CameraShape(
93
+ width=frame.width,
94
+ height=frame.height,
95
+ channel=frame.channels,
96
+ ),
97
+ )