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.
- luckyrobots/__init__.py +30 -12
- luckyrobots/client.py +997 -0
- luckyrobots/config/robots.yaml +231 -71
- luckyrobots/engine/__init__.py +23 -0
- luckyrobots/{utils → engine}/check_updates.py +108 -48
- luckyrobots/{utils → engine}/download.py +61 -39
- luckyrobots/engine/manager.py +427 -0
- luckyrobots/grpc/__init__.py +6 -0
- luckyrobots/grpc/generated/__init__.py +18 -0
- luckyrobots/grpc/generated/agent_pb2.py +69 -0
- luckyrobots/grpc/generated/agent_pb2_grpc.py +283 -0
- luckyrobots/grpc/generated/camera_pb2.py +47 -0
- luckyrobots/grpc/generated/camera_pb2_grpc.py +144 -0
- luckyrobots/grpc/generated/common_pb2.py +43 -0
- luckyrobots/grpc/generated/common_pb2_grpc.py +24 -0
- luckyrobots/grpc/generated/hazel_rpc_pb2.py +43 -0
- luckyrobots/grpc/generated/hazel_rpc_pb2_grpc.py +24 -0
- luckyrobots/grpc/generated/media_pb2.py +39 -0
- luckyrobots/grpc/generated/media_pb2_grpc.py +24 -0
- luckyrobots/grpc/generated/mujoco_pb2.py +51 -0
- luckyrobots/grpc/generated/mujoco_pb2_grpc.py +230 -0
- luckyrobots/grpc/generated/scene_pb2.py +66 -0
- luckyrobots/grpc/generated/scene_pb2_grpc.py +317 -0
- luckyrobots/grpc/generated/telemetry_pb2.py +47 -0
- luckyrobots/grpc/generated/telemetry_pb2_grpc.py +143 -0
- luckyrobots/grpc/generated/viewport_pb2.py +50 -0
- luckyrobots/grpc/generated/viewport_pb2_grpc.py +144 -0
- luckyrobots/grpc/proto/agent.proto +213 -0
- luckyrobots/grpc/proto/camera.proto +41 -0
- luckyrobots/grpc/proto/common.proto +36 -0
- luckyrobots/grpc/proto/hazel_rpc.proto +32 -0
- luckyrobots/grpc/proto/media.proto +26 -0
- luckyrobots/grpc/proto/mujoco.proto +64 -0
- luckyrobots/grpc/proto/scene.proto +104 -0
- luckyrobots/grpc/proto/telemetry.proto +43 -0
- luckyrobots/grpc/proto/viewport.proto +45 -0
- luckyrobots/luckyrobots.py +252 -0
- luckyrobots/models/__init__.py +15 -0
- luckyrobots/models/camera.py +97 -0
- luckyrobots/models/observation.py +135 -0
- luckyrobots/models/randomization.py +77 -0
- luckyrobots/{utils/helpers.py → utils.py} +75 -40
- luckyrobots-0.1.72.dist-info/METADATA +262 -0
- luckyrobots-0.1.72.dist-info/RECORD +47 -0
- {luckyrobots-0.1.63.dist-info → luckyrobots-0.1.72.dist-info}/WHEEL +1 -1
- luckyrobots/core/luckyrobots.py +0 -619
- luckyrobots/core/manager.py +0 -236
- luckyrobots/core/models.py +0 -68
- luckyrobots/core/node.py +0 -273
- luckyrobots/message/__init__.py +0 -18
- luckyrobots/message/pubsub.py +0 -145
- luckyrobots/message/srv/client.py +0 -81
- luckyrobots/message/srv/service.py +0 -135
- luckyrobots/message/srv/types.py +0 -83
- luckyrobots/message/transporter.py +0 -427
- luckyrobots/utils/event_loop.py +0 -94
- luckyrobots/utils/sim_manager.py +0 -406
- luckyrobots-0.1.63.dist-info/METADATA +0 -251
- luckyrobots-0.1.63.dist-info/RECORD +0 -24
- {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
|
+
)
|