luckyrobots 0.1.71__py3-none-any.whl → 0.1.73__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.
@@ -1,12 +1,6 @@
1
- """
2
- RL observation models for LuckyRobots.
1
+ """RL observation models for LuckyRobots."""
3
2
 
4
- These are the primary return types for the simplified API:
5
- - ObservationResponse: returned by get_observation()
6
- - StateSnapshot: returned by get_state()
7
- """
8
-
9
- from typing import Any, Dict, List, Optional
3
+ from typing import Dict, List, Optional
10
4
  from pydantic import BaseModel, Field, ConfigDict
11
5
 
12
6
 
@@ -14,11 +8,11 @@ class ObservationResponse(BaseModel):
14
8
  """RL observation data from an agent.
15
9
 
16
10
  This is the return type for LuckyEngineClient.get_observation() and
17
- LuckyRobots.get_observation(). It contains the RL observation vector
11
+ LuckyEngineClient.step(). It contains the RL observation vector
18
12
  with optional named access for debugging.
19
13
 
20
14
  Usage:
21
- obs = client.get_observation()
15
+ obs = client.step(actions)
22
16
 
23
17
  # Flat vector for RL training
24
18
  obs.observation # [0.1, 0.2, 0.3, ...]
@@ -110,26 +104,3 @@ class ObservationResponse(BaseModel):
110
104
  if self.action_names is not None:
111
105
  return dict(zip(self.action_names, self.actions))
112
106
  return {f"action_{i}": v for i, v in enumerate(self.actions)}
113
-
114
-
115
- class StateSnapshot(BaseModel):
116
- """Bundled snapshot of multiple data sources.
117
-
118
- Use LuckyEngineClient.get_state() to get a bundled snapshot when you need
119
- multiple data types in one efficient call. For streaming data like telemetry,
120
- use the dedicated streaming methods instead.
121
- """
122
-
123
- model_config = ConfigDict(frozen=True, arbitrary_types_allowed=True)
124
-
125
- observation: Optional[ObservationResponse] = Field(
126
- default=None, description="RL observation data (if include_observation=True)"
127
- )
128
- joint_state: Optional[Any] = Field(
129
- default=None, description="Joint positions/velocities (if include_joint_state=True)"
130
- )
131
- camera_frames: Optional[List[Any]] = Field(
132
- default=None, description="Camera frames (if camera_names provided)"
133
- )
134
- timestamp_ms: int = Field(default=0, description="Wall-clock timestamp in milliseconds")
135
- frame_number: int = Field(default=0, description="Monotonic frame counter")
luckyrobots/utils.py CHANGED
@@ -1,49 +1,7 @@
1
- """
2
- Utility functions and classes for LuckyRobots.
3
- """
1
+ """Utility functions for LuckyRobots."""
4
2
 
5
- import time
6
3
  import yaml
7
4
  import importlib.resources
8
- from collections import deque
9
-
10
-
11
- class FPS:
12
- """Utility for measuring frames per second with a rolling window.
13
-
14
- Usage:
15
- fps = FPS(frame_window=30)
16
- while running:
17
- # ... do work ...
18
- current_fps = fps.measure()
19
- """
20
-
21
- def __init__(self, frame_window: int = 30):
22
- """Initialize FPS counter.
23
-
24
- Args:
25
- frame_window: Number of frames to average over.
26
- """
27
- self.frame_window = frame_window
28
- self.frame_times: deque[float] = deque(maxlen=frame_window)
29
- self.last_frame_time = time.perf_counter()
30
-
31
- def measure(self) -> float:
32
- """Record a frame and return current FPS.
33
-
34
- Returns:
35
- Current frames per second (averaged over window).
36
- """
37
- current_time = time.perf_counter()
38
- frame_delta = current_time - self.last_frame_time
39
- self.last_frame_time = current_time
40
-
41
- self.frame_times.append(frame_delta)
42
-
43
- if len(self.frame_times) >= 2:
44
- avg_frame_time = sum(self.frame_times) / len(self.frame_times)
45
- return 1.0 / avg_frame_time if avg_frame_time > 0 else 0.0
46
- return 0.0
47
5
 
48
6
 
49
7
  def get_robot_config(robot: str = None) -> dict:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: luckyrobots
3
- Version: 0.1.71
3
+ Version: 0.1.73
4
4
  Summary: Robotics-AI Training in Hyperrealistic Game Environments
5
5
  Project-URL: Homepage, https://github.com/luckyrobots/luckyrobots
6
6
  Project-URL: Documentation, https://luckyrobots.readthedocs.io
@@ -1,17 +1,15 @@
1
- luckyrobots/__init__.py,sha256=VWewiYhYbYCs1ZSD0KxozmG2jH2uLlBxsVgFIRivKTM,824
2
- luckyrobots/client.py,sha256=0i8lVaLHjXXXzNZH3DWoc3ViqWBeoInrPP9nefkNWR4,32646
3
- luckyrobots/luckyrobots.py,sha256=YLx8mvuOOmY-ZaAve_k2md0HIGG84__5oXUXF1ymjhI,7812
4
- luckyrobots/utils.py,sha256=62BqpSRBFykVRMZ5Ti2F-yKRDoyU3UNHeesDF_PVCFc,2958
1
+ luckyrobots/__init__.py,sha256=cjma4V6iH-1QWWkt9n9Y9JD8LZJgmTIXP7eE9RT2ylw,469
2
+ luckyrobots/client.py,sha256=_OSK5WgMMcSUtSHwJRdmytB-WIJHYMazrwhkV5LSO0c,17667
3
+ luckyrobots/luckyrobots.py,sha256=-XkIt9h7si4GK_j5Wpknb7_h56CCWi8_d1woVMuIvxU,8957
4
+ luckyrobots/utils.py,sha256=deh0OV3I6Tr6V1s0yWs4jtchsZK1vKySpUohLjqOF1I,1769
5
5
  luckyrobots/config/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
- luckyrobots/config/robots.yaml,sha256=Wu2AgwhZxo9F-RLcqPJXHnqzjT8a0wR-rFBcecck_eE,5164
7
- luckyrobots/engine/__init__.py,sha256=-3r7aLueDF7eyumqIhTvYqS8ZNnVSJUJEHG4ybsKrjc,546
8
- luckyrobots/engine/check_updates.py,sha256=KGQHrj1LB0_vexktO01xx-ZvPOYKkto4VBb3H1KV7uI,7593
9
- luckyrobots/engine/download.py,sha256=vIb5T40Jk2BnfjPo0f4KgW14Om6P4_mvPpZ9JAlkVvg,4029
6
+ luckyrobots/config/robots.yaml,sha256=TevKlMzsZw-8k3PDf1aOG8zs18LedquS-ayVZeqyYdU,5968
7
+ luckyrobots/engine/__init__.py,sha256=OsyOgAnLtOfpRDHXa3Gzc1fRhJA8EfKvRkzijF6iRc8,403
10
8
  luckyrobots/engine/manager.py,sha256=8vxSevaoXo-lw2G6y74lVc-Ms8xIELK0yLswydMiOQY,13388
11
9
  luckyrobots/grpc/__init__.py,sha256=hPh-X_FEus2sxrfQB2fQjD754X5bMaUFTWm3mZmUhe0,153
12
10
  luckyrobots/grpc/generated/__init__.py,sha256=u69M4E_2LwRQSD_n4wvQyMX7dryBCrIdsho59c52Crk,732
13
- luckyrobots/grpc/generated/agent_pb2.py,sha256=me4OtOKGfwg5JUho6I5NV6v7ie-DO7T6iSCs8LcQ6yM,6555
14
- luckyrobots/grpc/generated/agent_pb2_grpc.py,sha256=VnjWZTKOE4KWRsy1n-ssfAk1kEP5uUqOhakWKzFtr4g,9123
11
+ luckyrobots/grpc/generated/agent_pb2.py,sha256=5rXekXme6u5KCtEtwjkhHkUyxrfxpMKutG4KPZqNr1U,7195
12
+ luckyrobots/grpc/generated/agent_pb2_grpc.py,sha256=M9ZsjS1qhBm57-DVy6il_u_XeTpb6Qbntm45Rqp_f1k,10909
15
13
  luckyrobots/grpc/generated/camera_pb2.py,sha256=2UDkgzMYdDZ53euPfLAndeI7k4wI9QH_Dvwkay6TAB4,2623
16
14
  luckyrobots/grpc/generated/camera_pb2_grpc.py,sha256=hUXzsf2qCE-ffZzkF8El2UuuLu6nxjKSYPukId2uFP8,5278
17
15
  luckyrobots/grpc/generated/common_pb2.py,sha256=0eZTxqlsZdTrgp2hgLy-hdDNtac5U3kkS_D9UbVy0e8,2045
@@ -22,26 +20,24 @@ luckyrobots/grpc/generated/media_pb2.py,sha256=TtGdBub2Pj1vG_jJJIZnIZealLD0s98wZ
22
20
  luckyrobots/grpc/generated/media_pb2_grpc.py,sha256=szeOZzqDnIBFEyjAtXEHNxytTb9hTvOkQj0PhvXumMM,885
23
21
  luckyrobots/grpc/generated/mujoco_pb2.py,sha256=Oq1AdZ0h-svs7ntlaDZ-zvQdTPsulnaE7YaINvBIBpM,3476
24
22
  luckyrobots/grpc/generated/mujoco_pb2_grpc.py,sha256=Rz8hyPvhB2nR-0Mej4Ly9ixbrdpPCxDafmL8H4x43Nk,8603
25
- luckyrobots/grpc/generated/scene_pb2.py,sha256=LzCApFBj0RXsFhvLYW07VXbuZubaiACj6rX8QwbGSG4,3981
26
- luckyrobots/grpc/generated/scene_pb2_grpc.py,sha256=A-aj6kPPydehlq1PhspITymVNktK0k3fd7xDKMU0Zy0,8569
23
+ luckyrobots/grpc/generated/scene_pb2.py,sha256=ZQyv_csWjB42WLfHNe8aQVMB5U9nQnJ7fY9enac9VGQ,5382
24
+ luckyrobots/grpc/generated/scene_pb2_grpc.py,sha256=I7G2LqkKZwEPZV-gxWo_45ecBey-wuGZ26pwxlH0mGQ,12118
27
25
  luckyrobots/grpc/generated/telemetry_pb2.py,sha256=LgVtxJv7ovtqlrgxG375aXADGt3qXwgpsIhIkJ3pLKE,2790
28
26
  luckyrobots/grpc/generated/telemetry_pb2_grpc.py,sha256=8TuCSc7rnQZFNqhEPXUoJsWAnwvEvyyEI5GaqQUCSSs,5463
29
27
  luckyrobots/grpc/generated/viewport_pb2.py,sha256=Z_5gBD23z7He_OACYkaIsi0o4LIESzOG7o_rO1lRLVs,3093
30
28
  luckyrobots/grpc/generated/viewport_pb2_grpc.py,sha256=BkJqREcjIa3ZdNgZUxH_MvBWOte9VJJGi-KlFgYepog,5436
31
- luckyrobots/grpc/proto/agent.proto,sha256=XOyjtsTCU9iIl-cXCWVOWynF-WTSDaMVY7EwVEDMEsE,7282
29
+ luckyrobots/grpc/proto/agent.proto,sha256=SUVR9EqNz5gB5FNt3n54sNXv3SkvYgXKywlZmZh5vio,8506
32
30
  luckyrobots/grpc/proto/camera.proto,sha256=unLowpk4d0AGJgyKrbzTr1F_CxOV9dXRoXn-gO8eJVw,1097
33
31
  luckyrobots/grpc/proto/common.proto,sha256=zQPj-Z8b8mAE2_eHlJ0L2_-JH-CYY_OJcv8046fAtQM,772
34
32
  luckyrobots/grpc/proto/hazel_rpc.proto,sha256=gtFukd1B9WRbY2kWBgd5YQWSw4cobYf1YRYX6VEaULE,1229
35
33
  luckyrobots/grpc/proto/media.proto,sha256=3Rhmiaw1zceQBFdwxX1ViFgH-cSr7mBjNEGHPaFmTsk,836
36
34
  luckyrobots/grpc/proto/mujoco.proto,sha256=b7tBvYqqvcSjmqrEax6_75-P3fIHHZq63A1HVJKjx0U,1873
37
- luckyrobots/grpc/proto/scene.proto,sha256=y19i9MYkqZQPfrvTZEwMH_LekYGOIjvo5qtCXh9_Xlg,1825
35
+ luckyrobots/grpc/proto/scene.proto,sha256=ZypWsUzpgplGsQPP0FXJpyY4rSxgWYGsniM4vofhnak,3075
38
36
  luckyrobots/grpc/proto/telemetry.proto,sha256=PZiUoA0bpwuvsBUdoQQdBNGWOuhlBMKAiBpwqPZiJgY,1481
39
37
  luckyrobots/grpc/proto/viewport.proto,sha256=gunF8cIrVgvoAX8FNER6gw_u-IlRWAR1Q6ts0zUuqr8,1324
40
- luckyrobots/models/__init__.py,sha256=assqJIKZXvDGwCVM0grNLJCBWyHtKRBAaxdJb2dON10,332
41
- luckyrobots/models/camera.py,sha256=e-CftAndHASfokhtUtU7ujGb2q_kcXZ6-2KKvY42CKg,3682
42
- luckyrobots/models/observation.py,sha256=jbRiUTjOP2ZiSobsnNcks3aMPAJA6oPoKqiXwvrNQK0,4758
43
- luckyrobots/models/randomization.py,sha256=AicqHovzG97Ge33wtNQE-_EvKxndjEHguTjCiHfhAV8,2753
44
- luckyrobots-0.1.71.dist-info/METADATA,sha256=9E-gglRuotJLzwUVxRy44pQlfDyCwKbImt_Zt7MwBT4,8379
45
- luckyrobots-0.1.71.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
46
- luckyrobots-0.1.71.dist-info/licenses/LICENSE,sha256=xsPYvRJPH_fW_sofTUknI_KvZOsD4-BqjSOTZqI6Nmw,1069
47
- luckyrobots-0.1.71.dist-info/RECORD,,
38
+ luckyrobots/models/__init__.py,sha256=h_PFZ1OgJnHXfQBqO8_ULB8szCKrGA4lMfmBGZgtIDc,126
39
+ luckyrobots/models/observation.py,sha256=H45zargSOMR4c-zPupbXNplWBTBfGdG-oQbib203eCY,3620
40
+ luckyrobots-0.1.73.dist-info/METADATA,sha256=GJY8BZbJzlwk_1VRViO_DmCZKoBxQjOxsAst7baVY38,8379
41
+ luckyrobots-0.1.73.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
42
+ luckyrobots-0.1.73.dist-info/licenses/LICENSE,sha256=xsPYvRJPH_fW_sofTUknI_KvZOsD4-BqjSOTZqI6Nmw,1069
43
+ luckyrobots-0.1.73.dist-info/RECORD,,
@@ -1,264 +0,0 @@
1
- """
2
- Check for LuckyEngine executable updates.
3
-
4
- This module compares local and remote file structures to detect changes.
5
- """
6
-
7
- import json
8
- import logging
9
- import mimetypes
10
- import os
11
- import platform
12
- import re
13
- import sys
14
- import zlib
15
- from typing import Optional
16
- from urllib.parse import urljoin
17
-
18
- import requests
19
-
20
- logger = logging.getLogger("luckyrobots.engine.check_updates")
21
-
22
- BASE_URL = "https://builds.luckyrobots.xyz/"
23
-
24
-
25
- def get_os_type() -> str:
26
- """
27
- Get the operating system type as a string.
28
-
29
- Returns:
30
- "mac", "win", or "linux"
31
-
32
- Raises:
33
- ValueError: If the OS is not supported.
34
- """
35
- os_type = platform.system().lower()
36
- if os_type == "darwin":
37
- return "mac"
38
- elif os_type == "windows":
39
- return "win"
40
- elif os_type == "linux":
41
- return "linux"
42
- else:
43
- raise ValueError(f"Unsupported operating system: {os_type}")
44
-
45
-
46
- def calculate_crc32(file_path: str) -> int:
47
- """
48
- Calculate CRC32 checksum for a file.
49
-
50
- Args:
51
- file_path: Path to the file.
52
-
53
- Returns:
54
- CRC32 checksum as unsigned 32-bit integer.
55
- """
56
- crc32 = 0
57
- with open(file_path, "rb") as file:
58
- while True:
59
- data = file.read(65536) # Read in 64kb chunks
60
- if not data:
61
- break
62
- crc32 = zlib.crc32(data, crc32)
63
- return crc32 & 0xFFFFFFFF # Ensure unsigned 32-bit integer
64
-
65
-
66
- def scan_directory(root_path: str) -> list[dict]:
67
- """
68
- Scan a directory and create a file structure representation.
69
-
70
- Args:
71
- root_path: Root directory to scan.
72
-
73
- Returns:
74
- List of dictionaries containing file/directory metadata.
75
- """
76
- file_structure = []
77
-
78
- for dirpath, dirnames, filenames in os.walk(root_path):
79
- # Add directories
80
- for dirname in dirnames:
81
- dir_path = os.path.join(dirpath, dirname)
82
- relative_path = os.path.relpath(dir_path, root_path)
83
- file_structure.append(
84
- {
85
- "path": relative_path,
86
- "type": "directory",
87
- "size": 0,
88
- "mtime": os.path.getmtime(dir_path),
89
- "crc32": 0,
90
- "mime_type": "directory",
91
- }
92
- )
93
-
94
- # Add files
95
- for filename in filenames:
96
- file_path = os.path.join(dirpath, filename)
97
- relative_path = os.path.relpath(file_path, root_path)
98
- file_stat = os.stat(file_path)
99
-
100
- # Guess the file type using mimetypes
101
- file_type, _ = mimetypes.guess_type(file_path)
102
- if file_type is None:
103
- file_type = "application/octet-stream"
104
-
105
- file_structure.append(
106
- {
107
- "path": relative_path,
108
- "crc32": calculate_crc32(file_path),
109
- "size": file_stat.st_size,
110
- "mtime": file_stat.st_mtime,
111
- "type": "file",
112
- "mime_type": file_type,
113
- }
114
- )
115
-
116
- return file_structure
117
-
118
-
119
- def save_json(data: dict, filename: str) -> None:
120
- """Save data to a JSON file."""
121
- with open(filename, "w") as f:
122
- json.dump(data, f, indent=2)
123
-
124
-
125
- def load_json(filename: str) -> dict:
126
- """Load data from a JSON file."""
127
- with open(filename, "r") as f:
128
- return json.load(f)
129
-
130
-
131
- def clean_path(path: str) -> tuple[str, Optional[str]]:
132
- """
133
- Clean and parse a path string.
134
-
135
- Args:
136
- path: Path string to clean.
137
-
138
- Returns:
139
- Tuple of (directory_path, attribute_name).
140
- """
141
- # Remove ['children'] and quotes, replace '][' with '/'
142
- cleaned = re.sub(r"\['children'\]", "", path).replace("']['", "/").strip("[]'")
143
- # Replace remaining single quotes with nothing
144
- cleaned = cleaned.replace("'", "")
145
- # Split the path and the attribute that changed
146
- parts = cleaned.rsplit("/", 1)
147
- return parts[0], parts[1] if len(parts) > 1 else None
148
-
149
-
150
- def compare_structures(
151
- client_structure: list[dict], server_structure: list[dict]
152
- ) -> list[dict]:
153
- """
154
- Compare two file structures and return list of changes.
155
-
156
- Args:
157
- client_structure: Local file structure.
158
- server_structure: Remote file structure.
159
-
160
- Returns:
161
- List of changes (new, modified, deleted files).
162
- """
163
- dict1 = {item["path"]: item for item in client_structure}
164
- dict2 = {item["path"]: item for item in server_structure}
165
-
166
- result = []
167
-
168
- # Check for new and modified items
169
- for path, item in dict2.items():
170
- if path not in dict1:
171
- item["change_type"] = "new_file"
172
- result.append(item)
173
- elif item["type"] == "file" and item["crc32"] != dict1[path]["crc32"]:
174
- item["change_type"] = "modified"
175
- result.append(item)
176
-
177
- # Check for deleted items
178
- for path, item in dict1.items():
179
- if path not in dict2:
180
- item["change_type"] = "deleted"
181
- result.append(item)
182
-
183
- # Remove hashmap.json from changes (it's metadata)
184
- result = [item for item in result if item["path"] != "hashmap.json"]
185
-
186
- return result
187
-
188
-
189
- def scan_server(server_path: str) -> None:
190
- """
191
- Scan server directory structure and save hashmap files.
192
-
193
- Args:
194
- server_path: Path to server root directory.
195
- """
196
- mac_path = os.path.join(server_path, "mac")
197
- win_path = os.path.join(server_path, "win")
198
- linux_path = os.path.join(server_path, "linux")
199
-
200
- mac_structure = scan_directory(mac_path)
201
- win_structure = scan_directory(win_path)
202
- linux_structure = scan_directory(linux_path)
203
-
204
- save_json(mac_structure, os.path.join(server_path, "mac/hashmap.json"))
205
- save_json(win_structure, os.path.join(server_path, "win/hashmap.json"))
206
- save_json(linux_structure, os.path.join(server_path, "linux/hashmap.json"))
207
-
208
-
209
- def check_updates(root_path: str, base_url: Optional[str] = None) -> list[dict]:
210
- """
211
- Check for updates by comparing local and remote file structures.
212
-
213
- Args:
214
- root_path: Local root directory to check.
215
- base_url: Base URL for remote server (uses default if None).
216
-
217
- Returns:
218
- List of changes detected.
219
- """
220
- if base_url is None:
221
- base_url = BASE_URL
222
-
223
- os_type = get_os_type()
224
- url = urljoin(base_url, f"{os_type}/hashmap.json")
225
-
226
- # Download the JSON file
227
- try:
228
- response = requests.get(url, timeout=10)
229
- response.raise_for_status()
230
- server_structure = response.json()
231
- except requests.RequestException as e:
232
- logger.warning(f"Error downloading JSON file: {e}")
233
- server_structure = None
234
-
235
- if server_structure is None:
236
- logger.info("Using local scan as no remote file could be downloaded.")
237
- server_structure = {}
238
-
239
- # Scan the directory and create a new file structure
240
- client_structure = scan_directory(root_path)
241
-
242
- # Compare and return changes
243
- if server_structure:
244
- changes = compare_structures(client_structure, server_structure)
245
- return changes
246
- else:
247
- logger.info("No server structure available for comparison.")
248
- return []
249
-
250
-
251
- if __name__ == "__main__":
252
- # This is used as a cron job on the main server to keep file structures up to date
253
- lr_server_root = None
254
-
255
- for arg in sys.argv:
256
- if arg.startswith("--lr-server-root="):
257
- lr_server_root = arg.split("=", 1)[1]
258
- break
259
-
260
- if lr_server_root:
261
- logger.info(f"Scanning server at {lr_server_root}")
262
- scan_server(lr_server_root)
263
- else:
264
- logger.error("--lr-server-root argument required")
@@ -1,125 +0,0 @@
1
- """
2
- Download and update LuckyEngine executable files.
3
-
4
- This module handles downloading updates and applying changes to the LuckyEngine binary.
5
- """
6
-
7
- import logging
8
- import os
9
- import platform
10
- from typing import Optional
11
-
12
- import requests
13
- from tqdm import tqdm
14
-
15
- from .check_updates import check_updates
16
-
17
- logger = logging.getLogger("luckyrobots.engine.download")
18
-
19
- BASE_URL = "https://builds.luckyrobots.xyz/"
20
-
21
-
22
- def get_base_url() -> str:
23
- """
24
- Get the base URL for downloads, checking local server first.
25
-
26
- Returns:
27
- Base URL string (local or remote).
28
- """
29
- local_url = "http://192.168.1.148/builds"
30
- remote_url = "https://builds.luckyrobots.xyz"
31
-
32
- try:
33
- response = requests.get(local_url, timeout=1)
34
- if response.status_code == 200:
35
- logger.info(f"Using local server: {local_url}")
36
- return local_url
37
- except requests.RequestException:
38
- pass
39
-
40
- logger.info(f"Using remote server: {remote_url}")
41
- return remote_url
42
-
43
-
44
- def get_os_type() -> str:
45
- """
46
- Get the operating system type as a string.
47
-
48
- Returns:
49
- "mac", "win", or "linux"
50
-
51
- Raises:
52
- ValueError: If the OS is not supported.
53
- """
54
- os_type = platform.system().lower()
55
- if os_type == "darwin":
56
- return "mac"
57
- elif os_type == "windows":
58
- return "win"
59
- elif os_type == "linux":
60
- return "linux"
61
- else:
62
- raise ValueError(f"Unsupported operating system: {os_type}")
63
-
64
-
65
- def apply_changes(changes: list[dict], binary_path: str = "./Binary") -> None:
66
- """
67
- Apply changes by downloading new/modified files and deleting removed files.
68
-
69
- Args:
70
- changes: List of change dictionaries with 'change_type', 'path', etc.
71
- binary_path: Base path where binary files are stored.
72
- """
73
- base_url = get_base_url()
74
- os_type = get_os_type()
75
-
76
- for item in changes:
77
- change_type = item.get("change_type")
78
- item_path = os.path.join(binary_path, item["path"])
79
-
80
- if change_type in ["modified", "new_file"]:
81
- if item.get("type") == "directory":
82
- # Create the directory
83
- os.makedirs(item_path, exist_ok=True)
84
- logger.debug(f"Created directory: {item_path}")
85
- else:
86
- # Handle file download with progress bar
87
- file_url = f"{base_url}{os_type}/{item['path']}"
88
-
89
- # Ensure the directory exists
90
- item_dir = os.path.dirname(item_path)
91
- os.makedirs(item_dir, exist_ok=True)
92
-
93
- # Download the file with progress bar
94
- try:
95
- response = requests.get(file_url, stream=True, timeout=30)
96
- response.raise_for_status()
97
- total_size = int(response.headers.get("content-length", 0))
98
-
99
- with open(item_path, "wb") as f, tqdm(
100
- desc=f"{item['path'][:8]}...{item['path'][-16:]}",
101
- total=total_size,
102
- unit="iB",
103
- unit_scale=True,
104
- unit_divisor=1024,
105
- ascii=" ▆",
106
- ) as progress_bar:
107
- for data in response.iter_content(chunk_size=1024):
108
- size = f.write(data)
109
- progress_bar.update(size)
110
-
111
- logger.debug(f"Downloaded: {item_path}")
112
- except requests.RequestException as e:
113
- logger.error(f"Error downloading {item_path}: {e}")
114
-
115
- elif change_type == "deleted":
116
- # Delete the file or directory
117
- try:
118
- if os.path.isdir(item_path):
119
- os.rmdir(item_path)
120
- logger.debug(f"Deleted directory: {item_path}")
121
- else:
122
- os.remove(item_path)
123
- logger.debug(f"Deleted file: {item_path}")
124
- except OSError as e:
125
- logger.error(f"Error deleting {item_path}: {e}")
@@ -1,97 +0,0 @@
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
- )