antioch-py 2.0.6__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.
Potentially problematic release.
This version of antioch-py might be problematic. Click here for more details.
- antioch/__init__.py +0 -0
- antioch/message.py +87 -0
- antioch/module/__init__.py +53 -0
- antioch/module/clock.py +62 -0
- antioch/module/execution.py +278 -0
- antioch/module/input.py +127 -0
- antioch/module/module.py +218 -0
- antioch/module/node.py +357 -0
- antioch/module/token.py +42 -0
- antioch/session/__init__.py +150 -0
- antioch/session/ark.py +504 -0
- antioch/session/asset.py +65 -0
- antioch/session/error.py +80 -0
- antioch/session/record.py +158 -0
- antioch/session/scene.py +1521 -0
- antioch/session/session.py +220 -0
- antioch/session/task.py +323 -0
- antioch/session/views/__init__.py +40 -0
- antioch/session/views/animation.py +189 -0
- antioch/session/views/articulation.py +245 -0
- antioch/session/views/basis_curve.py +186 -0
- antioch/session/views/camera.py +92 -0
- antioch/session/views/collision.py +75 -0
- antioch/session/views/geometry.py +74 -0
- antioch/session/views/ground_plane.py +63 -0
- antioch/session/views/imu.py +73 -0
- antioch/session/views/joint.py +64 -0
- antioch/session/views/light.py +175 -0
- antioch/session/views/pir_sensor.py +140 -0
- antioch/session/views/radar.py +73 -0
- antioch/session/views/rigid_body.py +282 -0
- antioch/session/views/xform.py +119 -0
- antioch_py-2.0.6.dist-info/METADATA +115 -0
- antioch_py-2.0.6.dist-info/RECORD +99 -0
- antioch_py-2.0.6.dist-info/WHEEL +5 -0
- antioch_py-2.0.6.dist-info/entry_points.txt +2 -0
- antioch_py-2.0.6.dist-info/top_level.txt +2 -0
- common/__init__.py +0 -0
- common/ark/__init__.py +60 -0
- common/ark/ark.py +128 -0
- common/ark/hardware.py +121 -0
- common/ark/kinematics.py +31 -0
- common/ark/module.py +85 -0
- common/ark/node.py +94 -0
- common/ark/scheduler.py +439 -0
- common/ark/sim.py +33 -0
- common/assets/__init__.py +3 -0
- common/constants.py +47 -0
- common/core/__init__.py +52 -0
- common/core/agent.py +296 -0
- common/core/auth.py +305 -0
- common/core/registry.py +331 -0
- common/core/task.py +36 -0
- common/message/__init__.py +59 -0
- common/message/annotation.py +89 -0
- common/message/array.py +500 -0
- common/message/base.py +517 -0
- common/message/camera.py +91 -0
- common/message/color.py +139 -0
- common/message/frame.py +50 -0
- common/message/image.py +171 -0
- common/message/imu.py +14 -0
- common/message/joint.py +47 -0
- common/message/log.py +31 -0
- common/message/pir.py +16 -0
- common/message/point.py +109 -0
- common/message/point_cloud.py +63 -0
- common/message/pose.py +148 -0
- common/message/quaternion.py +273 -0
- common/message/radar.py +58 -0
- common/message/types.py +37 -0
- common/message/vector.py +786 -0
- common/rome/__init__.py +9 -0
- common/rome/client.py +430 -0
- common/rome/error.py +16 -0
- common/session/__init__.py +54 -0
- common/session/environment.py +31 -0
- common/session/sim.py +240 -0
- common/session/views/__init__.py +263 -0
- common/session/views/animation.py +73 -0
- common/session/views/articulation.py +184 -0
- common/session/views/basis_curve.py +102 -0
- common/session/views/camera.py +147 -0
- common/session/views/collision.py +59 -0
- common/session/views/geometry.py +102 -0
- common/session/views/ground_plane.py +41 -0
- common/session/views/imu.py +66 -0
- common/session/views/joint.py +81 -0
- common/session/views/light.py +96 -0
- common/session/views/pir_sensor.py +115 -0
- common/session/views/radar.py +82 -0
- common/session/views/rigid_body.py +236 -0
- common/session/views/viewport.py +21 -0
- common/session/views/xform.py +39 -0
- common/utils/__init__.py +4 -0
- common/utils/comms.py +571 -0
- common/utils/logger.py +123 -0
- common/utils/time.py +42 -0
- common/utils/usd.py +12 -0
common/message/base.py
ADDED
|
@@ -0,0 +1,517 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import re
|
|
3
|
+
from abc import ABC
|
|
4
|
+
from typing import Any, ClassVar, TypeVar
|
|
5
|
+
|
|
6
|
+
import ormsgpack
|
|
7
|
+
import yaml
|
|
8
|
+
from pydantic import BaseModel, ValidationError
|
|
9
|
+
|
|
10
|
+
T = TypeVar("T", bound="Message")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class MessageError(Exception):
|
|
14
|
+
"""
|
|
15
|
+
Base exception for Message errors.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class SerializationError(MessageError):
|
|
20
|
+
"""
|
|
21
|
+
Raised when serialization (pack, to_json) fails.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class DeserializationError(MessageError):
|
|
26
|
+
"""
|
|
27
|
+
Raised when deserialization (unpack, from_json) fails.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class FileAccessError(MessageError):
|
|
32
|
+
"""
|
|
33
|
+
Raised when file operations fail.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class MismatchError(MessageError):
|
|
38
|
+
"""
|
|
39
|
+
Raised when type doesn't match expected type or deserialization fails.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(self, expected: str | None, actual: str | None, details: str | None = None):
|
|
43
|
+
self.expected = expected
|
|
44
|
+
self.actual = actual
|
|
45
|
+
self.details = details
|
|
46
|
+
|
|
47
|
+
expected_str = "<untyped>" if expected is None else f"'{expected}'"
|
|
48
|
+
actual_str = "<untyped>" if actual is None else f"'{actual}'"
|
|
49
|
+
message = f"Type mismatch: expected {expected_str}, got {actual_str}"
|
|
50
|
+
if details is not None:
|
|
51
|
+
message += f" - {details}"
|
|
52
|
+
|
|
53
|
+
super().__init__(message)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class Message(BaseModel, ABC):
|
|
57
|
+
"""
|
|
58
|
+
Base class for user-defined data types supporting serialization and deserialization.
|
|
59
|
+
|
|
60
|
+
To add a type identifier, simply set a '_type' class variable. Otherwise, the message will
|
|
61
|
+
have type=null in the serialized format.
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
class MyMessage(Message):
|
|
65
|
+
_type = "my_message"
|
|
66
|
+
field1: str
|
|
67
|
+
field2: int
|
|
68
|
+
```
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
_type: ClassVar[str | None] = None
|
|
72
|
+
|
|
73
|
+
model_config = {
|
|
74
|
+
"extra": "forbid",
|
|
75
|
+
"frozen": False,
|
|
76
|
+
"arbitrary_types_allowed": True,
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
def __init_subclass__(cls, **kwargs):
|
|
80
|
+
"""
|
|
81
|
+
Validate type format when subclass is defined.
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
super().__init_subclass__(**kwargs)
|
|
85
|
+
if cls._type is not None and not re.compile(r"^[a-z0-9][a-z0-9\-_/]*$").match(cls._type):
|
|
86
|
+
raise ValueError(
|
|
87
|
+
f"Invalid type '{cls._type}' for {cls.__name__} (must be lowercase alphanumeric with hyphens, underscores, or slashes)"
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
def __str__(self) -> str:
|
|
91
|
+
"""
|
|
92
|
+
Return a clean string representation of the message.
|
|
93
|
+
|
|
94
|
+
:return: A formatted string showing the message type and field values.
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
# Create key=value pairs for all fields
|
|
98
|
+
field_strs = []
|
|
99
|
+
for field_name, field_value in self:
|
|
100
|
+
formatted_value = f'"{field_value}"' if isinstance(field_value, str) else str(field_value)
|
|
101
|
+
field_strs.append(f"{field_name}={formatted_value}")
|
|
102
|
+
|
|
103
|
+
return f"<{self.__class__.__name__}: {' '.join(field_strs)}>"
|
|
104
|
+
|
|
105
|
+
def __repr__(self) -> str:
|
|
106
|
+
"""
|
|
107
|
+
Return a detailed representation that can reconstruct the object.
|
|
108
|
+
|
|
109
|
+
:return: A string representation suitable for debugging.
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
return super().__repr__()
|
|
113
|
+
|
|
114
|
+
def __hash__(self) -> int:
|
|
115
|
+
"""
|
|
116
|
+
Make Message hashable for use in sets and as dict keys.
|
|
117
|
+
|
|
118
|
+
:return: Hash value based on class type, type, and field values.
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
field_items = sorted(self.model_dump().items())
|
|
122
|
+
return hash((self.__class__.__name__, self.get_type(), tuple(field_items)))
|
|
123
|
+
|
|
124
|
+
def __eq__(self, other: Any) -> bool:
|
|
125
|
+
"""
|
|
126
|
+
Compare messages based on class, type identifier, and field values.
|
|
127
|
+
|
|
128
|
+
:param other: The other object to compare with.
|
|
129
|
+
:return: True if messages are equal, False otherwise.
|
|
130
|
+
"""
|
|
131
|
+
|
|
132
|
+
if not isinstance(other, self.__class__):
|
|
133
|
+
return False
|
|
134
|
+
if self.get_type() != other.get_type():
|
|
135
|
+
return False
|
|
136
|
+
|
|
137
|
+
return self.model_dump() == other.model_dump()
|
|
138
|
+
|
|
139
|
+
@classmethod
|
|
140
|
+
def get_type(cls) -> str | None:
|
|
141
|
+
"""
|
|
142
|
+
Get the type identifier for this message class.
|
|
143
|
+
|
|
144
|
+
:return: The type string if set, None otherwise.
|
|
145
|
+
"""
|
|
146
|
+
|
|
147
|
+
return cls._type
|
|
148
|
+
|
|
149
|
+
@staticmethod
|
|
150
|
+
def pack_json(data: dict[str, Any]) -> bytes:
|
|
151
|
+
"""
|
|
152
|
+
Pack an arbitrary dict into a message envelope with null type.
|
|
153
|
+
|
|
154
|
+
This allows flexible telemetry without requiring Message implementations.
|
|
155
|
+
The dict is wrapped in an envelope with type=None and serialized to MessagePack.
|
|
156
|
+
|
|
157
|
+
:param data: Dictionary to pack.
|
|
158
|
+
:return: The MessagePack bytes.
|
|
159
|
+
:raises SerializationError: If serialization fails.
|
|
160
|
+
"""
|
|
161
|
+
|
|
162
|
+
try:
|
|
163
|
+
envelope = {"type": None, "data": data}
|
|
164
|
+
return ormsgpack.packb(envelope)
|
|
165
|
+
except Exception as e:
|
|
166
|
+
raise SerializationError(f"Failed to serialize dict: {e}") from None
|
|
167
|
+
|
|
168
|
+
@classmethod
|
|
169
|
+
def unpack(cls: type[T], data: bytes) -> T:
|
|
170
|
+
"""
|
|
171
|
+
Deserialize a message from bytes using MessagePack.
|
|
172
|
+
|
|
173
|
+
This validates that the type in the envelope matches the expected type
|
|
174
|
+
for this class.
|
|
175
|
+
|
|
176
|
+
:param data: The MessagePack bytes to deserialize.
|
|
177
|
+
:return: The deserialized message.
|
|
178
|
+
:raises DeserializationError: If deserialization fails.
|
|
179
|
+
:raises MismatchError: If the type doesn't match.
|
|
180
|
+
"""
|
|
181
|
+
|
|
182
|
+
try:
|
|
183
|
+
envelope = ormsgpack.unpackb(data)
|
|
184
|
+
cls._validate_envelope(envelope)
|
|
185
|
+
|
|
186
|
+
# Check type matches
|
|
187
|
+
if envelope["type"] != cls.get_type():
|
|
188
|
+
raise MismatchError(expected=cls.get_type(), actual=envelope["type"])
|
|
189
|
+
|
|
190
|
+
return cls(**envelope["data"])
|
|
191
|
+
except (MismatchError, DeserializationError):
|
|
192
|
+
raise
|
|
193
|
+
except ValidationError as e:
|
|
194
|
+
first_error = e.errors()[0]
|
|
195
|
+
field_path = " -> ".join(str(loc) for loc in first_error["loc"])
|
|
196
|
+
error_msg = first_error["msg"]
|
|
197
|
+
raise DeserializationError(f"Validation failed at {field_path}: {error_msg}") from None
|
|
198
|
+
except Exception as e:
|
|
199
|
+
if "msgpack" in str(e).lower() or "unpackb" in str(e):
|
|
200
|
+
raise DeserializationError("Failed to deserialize message") from None
|
|
201
|
+
raise DeserializationError(f"Failed to deserialize message: {e}") from None
|
|
202
|
+
|
|
203
|
+
@classmethod
|
|
204
|
+
def from_json(cls: type[T], json_str: str) -> T:
|
|
205
|
+
"""
|
|
206
|
+
Deserialize a message from a JSON string.
|
|
207
|
+
|
|
208
|
+
This validates that the type in the envelope matches the expected type.
|
|
209
|
+
|
|
210
|
+
:param json_str: The JSON string to deserialize.
|
|
211
|
+
:return: The deserialized message.
|
|
212
|
+
:raises DeserializationError: If deserialization fails or JSON is invalid.
|
|
213
|
+
:raises MismatchError: If the type doesn't match.
|
|
214
|
+
"""
|
|
215
|
+
|
|
216
|
+
try:
|
|
217
|
+
envelope = json.loads(json_str)
|
|
218
|
+
cls._validate_envelope(envelope)
|
|
219
|
+
|
|
220
|
+
# Check type matches
|
|
221
|
+
if envelope["type"] != cls.get_type():
|
|
222
|
+
raise MismatchError(expected=cls.get_type(), actual=envelope["type"])
|
|
223
|
+
|
|
224
|
+
return cls(**envelope["data"])
|
|
225
|
+
except json.JSONDecodeError:
|
|
226
|
+
raise DeserializationError("Invalid JSON format") from None
|
|
227
|
+
except (MismatchError, DeserializationError):
|
|
228
|
+
raise
|
|
229
|
+
except ValidationError as e:
|
|
230
|
+
first_error = e.errors()[0]
|
|
231
|
+
field_path = " -> ".join(str(loc) for loc in first_error["loc"])
|
|
232
|
+
error_msg = first_error["msg"]
|
|
233
|
+
raise DeserializationError(f"Validation failed at {field_path}: {error_msg}") from None
|
|
234
|
+
except Exception as e:
|
|
235
|
+
raise DeserializationError(f"Failed to deserialize from JSON: {e}") from None
|
|
236
|
+
|
|
237
|
+
@classmethod
|
|
238
|
+
def from_yaml(cls: type[T], yaml_str: str) -> T:
|
|
239
|
+
"""
|
|
240
|
+
Deserialize a message from a YAML string.
|
|
241
|
+
|
|
242
|
+
This validates that the type in the envelope matches the expected type.
|
|
243
|
+
|
|
244
|
+
:param yaml_str: The YAML string to deserialize.
|
|
245
|
+
:return: The deserialized message.
|
|
246
|
+
:raises DeserializationError: If deserialization fails or YAML is invalid.
|
|
247
|
+
:raises MismatchError: If the type doesn't match.
|
|
248
|
+
"""
|
|
249
|
+
|
|
250
|
+
try:
|
|
251
|
+
envelope = yaml.safe_load(yaml_str)
|
|
252
|
+
cls._validate_envelope(envelope)
|
|
253
|
+
|
|
254
|
+
# Check type matches
|
|
255
|
+
if envelope["type"] != cls.get_type():
|
|
256
|
+
raise MismatchError(expected=cls.get_type(), actual=envelope["type"])
|
|
257
|
+
|
|
258
|
+
return cls(**envelope["data"])
|
|
259
|
+
except yaml.YAMLError as e:
|
|
260
|
+
raise DeserializationError(f"Invalid YAML format: {e}") from None
|
|
261
|
+
except (MismatchError, DeserializationError):
|
|
262
|
+
raise
|
|
263
|
+
except ValidationError as e:
|
|
264
|
+
first_error = e.errors()[0]
|
|
265
|
+
field_path = " -> ".join(str(loc) for loc in first_error["loc"])
|
|
266
|
+
error_msg = first_error["msg"]
|
|
267
|
+
raise DeserializationError(f"Validation failed at {field_path}: {error_msg}") from None
|
|
268
|
+
except Exception as e:
|
|
269
|
+
raise DeserializationError(f"Failed to deserialize from YAML: {e}") from None
|
|
270
|
+
|
|
271
|
+
@classmethod
|
|
272
|
+
def load(cls: type[T], file_path: str, format: str | None = None) -> T:
|
|
273
|
+
"""
|
|
274
|
+
Load a message from a file.
|
|
275
|
+
|
|
276
|
+
The format is determined by the file extension if not specified.
|
|
277
|
+
Supported formats: json, yaml, msgpack
|
|
278
|
+
|
|
279
|
+
:param file_path: Path to load the file from.
|
|
280
|
+
:param format: Optional format override ('json', 'yaml', 'msgpack').
|
|
281
|
+
:return: The loaded message.
|
|
282
|
+
:raises FileAccessError: If the file cannot be read.
|
|
283
|
+
:raises DeserializationError: If deserialization fails.
|
|
284
|
+
:raises MismatchError: If the type doesn't match.
|
|
285
|
+
"""
|
|
286
|
+
|
|
287
|
+
# Determine format from extension if not specified
|
|
288
|
+
if format is None:
|
|
289
|
+
if file_path.endswith(".json"):
|
|
290
|
+
format = "json"
|
|
291
|
+
elif file_path.endswith((".yaml", ".yml")):
|
|
292
|
+
format = "yaml"
|
|
293
|
+
elif file_path.endswith((".msgpack", ".mp")):
|
|
294
|
+
format = "msgpack"
|
|
295
|
+
else:
|
|
296
|
+
raise FileAccessError(f"Cannot determine format from extension: {file_path}")
|
|
297
|
+
|
|
298
|
+
try:
|
|
299
|
+
# Read and deserialize based on format
|
|
300
|
+
if format == "json":
|
|
301
|
+
with open(file_path) as f:
|
|
302
|
+
return cls.from_json(f.read())
|
|
303
|
+
elif format == "yaml":
|
|
304
|
+
with open(file_path) as f:
|
|
305
|
+
return cls.from_yaml(f.read())
|
|
306
|
+
elif format == "msgpack":
|
|
307
|
+
with open(file_path, "rb") as f:
|
|
308
|
+
return cls.unpack(f.read())
|
|
309
|
+
else:
|
|
310
|
+
raise FileAccessError(f"Unsupported format: {format}")
|
|
311
|
+
except (DeserializationError, MismatchError):
|
|
312
|
+
raise
|
|
313
|
+
except FileNotFoundError:
|
|
314
|
+
raise FileAccessError(f"File not found: {file_path}") from None
|
|
315
|
+
except Exception as e:
|
|
316
|
+
raise FileAccessError(f"Failed to load from {file_path}: {e}") from None
|
|
317
|
+
|
|
318
|
+
@staticmethod
|
|
319
|
+
def extract_type(data: bytes) -> str | None:
|
|
320
|
+
"""
|
|
321
|
+
Extract the type identifier from a serialized message without full deserialization.
|
|
322
|
+
|
|
323
|
+
This is useful for dynamic message routing where you need to determine the message
|
|
324
|
+
type before deciding how to deserialize it.
|
|
325
|
+
|
|
326
|
+
:param data: The MessagePack bytes to extract type from.
|
|
327
|
+
:return: The type string, or None if no type is set.
|
|
328
|
+
:raises DeserializationError: If the data cannot be parsed.
|
|
329
|
+
|
|
330
|
+
Example:
|
|
331
|
+
```python
|
|
332
|
+
packed = some_message.pack()
|
|
333
|
+
msg_type = Message.extract_type(packed)
|
|
334
|
+
match msg_type:
|
|
335
|
+
case "user_profile":
|
|
336
|
+
profile = UserProfile.unpack(packed)
|
|
337
|
+
case "order_update":
|
|
338
|
+
order = OrderUpdate.unpack(packed)
|
|
339
|
+
case _:
|
|
340
|
+
raise ValueError(f"Unknown message type: {msg_type}")
|
|
341
|
+
```
|
|
342
|
+
"""
|
|
343
|
+
|
|
344
|
+
try:
|
|
345
|
+
envelope = ormsgpack.unpackb(data)
|
|
346
|
+
Message._validate_envelope(envelope)
|
|
347
|
+
return envelope["type"]
|
|
348
|
+
except (ValueError, TypeError) as e:
|
|
349
|
+
raise DeserializationError(f"Failed to extract message type: {e}") from None
|
|
350
|
+
except RecursionError:
|
|
351
|
+
raise DeserializationError("Message structure too deeply nested") from None
|
|
352
|
+
|
|
353
|
+
@staticmethod
|
|
354
|
+
def extract_type_from_json(json_str: str) -> str | None:
|
|
355
|
+
"""
|
|
356
|
+
Extract the type identifier from a JSON string without full deserialization.
|
|
357
|
+
|
|
358
|
+
This is the JSON equivalent of extract_type for when messages are
|
|
359
|
+
serialized as JSON instead of MessagePack.
|
|
360
|
+
|
|
361
|
+
:param json_str: The JSON string to extract type from.
|
|
362
|
+
:return: The type string, or None if no type is set.
|
|
363
|
+
:raises DeserializationError: If the JSON cannot be parsed.
|
|
364
|
+
"""
|
|
365
|
+
|
|
366
|
+
try:
|
|
367
|
+
data = json.loads(json_str)
|
|
368
|
+
Message._validate_envelope(data)
|
|
369
|
+
return data["type"]
|
|
370
|
+
except json.JSONDecodeError:
|
|
371
|
+
raise DeserializationError("Invalid JSON format") from None
|
|
372
|
+
|
|
373
|
+
@staticmethod
|
|
374
|
+
def extract_data_as_json(data: bytes) -> dict:
|
|
375
|
+
"""
|
|
376
|
+
Extract the data field from a serialized message as a dictionary.
|
|
377
|
+
|
|
378
|
+
Useful for generic message handling where the exact type is unknown.
|
|
379
|
+
|
|
380
|
+
:param data: The MessagePack bytes to extract data from.
|
|
381
|
+
:return: The data field as a dictionary.
|
|
382
|
+
:raises DeserializationError: If the data cannot be parsed.
|
|
383
|
+
"""
|
|
384
|
+
|
|
385
|
+
try:
|
|
386
|
+
envelope = ormsgpack.unpackb(data)
|
|
387
|
+
Message._validate_envelope(envelope)
|
|
388
|
+
return envelope["data"]
|
|
389
|
+
except Exception as e:
|
|
390
|
+
raise DeserializationError(f"Failed to extract message data: {e}") from None
|
|
391
|
+
|
|
392
|
+
def pack(self) -> bytes:
|
|
393
|
+
"""
|
|
394
|
+
Serialize the message to bytes using MessagePack.
|
|
395
|
+
|
|
396
|
+
The message is wrapped in an envelope with the type identifier before
|
|
397
|
+
serialization to match the Rust format.
|
|
398
|
+
|
|
399
|
+
:return: The MessagePack bytes.
|
|
400
|
+
:raises SerializationError: If serialization fails.
|
|
401
|
+
"""
|
|
402
|
+
|
|
403
|
+
try:
|
|
404
|
+
envelope = {
|
|
405
|
+
"type": self.get_type(),
|
|
406
|
+
"data": self.model_dump(mode="python", by_alias=True),
|
|
407
|
+
}
|
|
408
|
+
return ormsgpack.packb(envelope)
|
|
409
|
+
except Exception as e:
|
|
410
|
+
raise SerializationError(f"Failed to serialize message: {e}") from None
|
|
411
|
+
|
|
412
|
+
def to_json(self, indent: int | None = None) -> str:
|
|
413
|
+
"""
|
|
414
|
+
Serialize the message to a JSON string.
|
|
415
|
+
|
|
416
|
+
The message is wrapped in an envelope with the type identifier.
|
|
417
|
+
|
|
418
|
+
:param indent: Number of spaces to indent for pretty printing.
|
|
419
|
+
:return: The JSON string.
|
|
420
|
+
:raises SerializationError: If serialization fails.
|
|
421
|
+
"""
|
|
422
|
+
|
|
423
|
+
try:
|
|
424
|
+
envelope = {
|
|
425
|
+
"type": self.get_type(),
|
|
426
|
+
"data": self.model_dump(mode="python", by_alias=True),
|
|
427
|
+
}
|
|
428
|
+
return json.dumps(envelope, indent=indent)
|
|
429
|
+
except Exception as e:
|
|
430
|
+
raise SerializationError(f"Failed to serialize to JSON: {e}") from e
|
|
431
|
+
|
|
432
|
+
def to_yaml(self) -> str:
|
|
433
|
+
"""
|
|
434
|
+
Serialize the message to a YAML string.
|
|
435
|
+
|
|
436
|
+
The message is wrapped in an envelope with the type identifier.
|
|
437
|
+
|
|
438
|
+
:return: The YAML string.
|
|
439
|
+
:raises SerializationError: If serialization fails.
|
|
440
|
+
"""
|
|
441
|
+
|
|
442
|
+
try:
|
|
443
|
+
return yaml.dump(self.to_dict(), default_flow_style=False, sort_keys=False)
|
|
444
|
+
except Exception as e:
|
|
445
|
+
raise SerializationError(f"Failed to serialize to YAML: {e}") from None
|
|
446
|
+
|
|
447
|
+
def to_dict(self) -> dict[str, Any]:
|
|
448
|
+
"""
|
|
449
|
+
Serialize with standard envelope structure.
|
|
450
|
+
|
|
451
|
+
:return: The serialized object.
|
|
452
|
+
"""
|
|
453
|
+
|
|
454
|
+
return {
|
|
455
|
+
"type": self.get_type(),
|
|
456
|
+
"data": self.model_dump(mode="python", by_alias=True),
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
def save(self, file_path: str, format: str | None = None) -> None:
|
|
460
|
+
"""
|
|
461
|
+
Save the message to a file.
|
|
462
|
+
|
|
463
|
+
The format is determined by the file extension if not specified.
|
|
464
|
+
Supported formats: json, yaml, msgpack
|
|
465
|
+
|
|
466
|
+
:param file_path: Path to save the file to.
|
|
467
|
+
:param format: Optional format override ('json', 'yaml', 'msgpack').
|
|
468
|
+
:raises FileAccessError: If the file cannot be written.
|
|
469
|
+
:raises SerializationError: If serialization fails.
|
|
470
|
+
"""
|
|
471
|
+
|
|
472
|
+
# Determine format from extension if not specified
|
|
473
|
+
if format is None:
|
|
474
|
+
if file_path.endswith(".json"):
|
|
475
|
+
format = "json"
|
|
476
|
+
elif file_path.endswith((".yaml", ".yml")):
|
|
477
|
+
format = "yaml"
|
|
478
|
+
elif file_path.endswith((".msgpack", ".mp")):
|
|
479
|
+
format = "msgpack"
|
|
480
|
+
else:
|
|
481
|
+
raise FileAccessError(f"Cannot determine format from extension: {file_path}")
|
|
482
|
+
|
|
483
|
+
try:
|
|
484
|
+
# Serialize based on format
|
|
485
|
+
if format == "json":
|
|
486
|
+
content = self.to_json(indent=2)
|
|
487
|
+
mode = "w"
|
|
488
|
+
elif format == "yaml":
|
|
489
|
+
content = self.to_yaml()
|
|
490
|
+
mode = "w"
|
|
491
|
+
elif format == "msgpack":
|
|
492
|
+
content = self.pack()
|
|
493
|
+
mode = "wb"
|
|
494
|
+
else:
|
|
495
|
+
raise FileAccessError(f"Unsupported format: {format}")
|
|
496
|
+
|
|
497
|
+
# Write to file
|
|
498
|
+
with open(file_path, mode) as f:
|
|
499
|
+
f.write(content)
|
|
500
|
+
except SerializationError:
|
|
501
|
+
raise
|
|
502
|
+
except Exception as e:
|
|
503
|
+
raise FileAccessError(f"Failed to save to {file_path}: {e}") from None
|
|
504
|
+
|
|
505
|
+
@staticmethod
|
|
506
|
+
def _validate_envelope(envelope: Any) -> None:
|
|
507
|
+
"""
|
|
508
|
+
Validate envelope structure.
|
|
509
|
+
|
|
510
|
+
:param envelope: The envelope to validate.
|
|
511
|
+
:raises DeserializationError: If envelope structure is invalid.
|
|
512
|
+
"""
|
|
513
|
+
|
|
514
|
+
if not isinstance(envelope, dict):
|
|
515
|
+
raise DeserializationError("Invalid message format: expected dictionary")
|
|
516
|
+
if "type" not in envelope or "data" not in envelope:
|
|
517
|
+
raise DeserializationError("Invalid message format: missing 'type' or 'data' field")
|
common/message/camera.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from pydantic import Field
|
|
3
|
+
|
|
4
|
+
from common.message.base import Message
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class CameraInfo(Message):
|
|
8
|
+
"""
|
|
9
|
+
Complete camera information including intrinsics, distortion, and projection.
|
|
10
|
+
|
|
11
|
+
Follows standard camera calibration conventions with support for various
|
|
12
|
+
distortion models and projection operations.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
_type = "antioch/camera_info"
|
|
16
|
+
|
|
17
|
+
# Image dimensions
|
|
18
|
+
width: int = Field(description="Image width in pixels")
|
|
19
|
+
height: int = Field(description="Image height in pixels")
|
|
20
|
+
|
|
21
|
+
# Intrinsic parameters
|
|
22
|
+
fx: float = Field(description="Focal length in x (pixels)")
|
|
23
|
+
fy: float = Field(description="Focal length in y (pixels)")
|
|
24
|
+
cx: float = Field(description="Principal point x (pixels)")
|
|
25
|
+
cy: float = Field(description="Principal point y (pixels)")
|
|
26
|
+
|
|
27
|
+
# Distortion model and coefficients
|
|
28
|
+
distortion_model: str = Field(default="pinhole", description="Distortion model name")
|
|
29
|
+
distortion_coefficients: list[float] = Field(default_factory=list, description="Distortion coefficients")
|
|
30
|
+
|
|
31
|
+
# Frame information
|
|
32
|
+
frame_id: str = Field(default="camera_optical_frame", description="Camera coordinate frame")
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def intrinsics_matrix(self) -> np.ndarray:
|
|
36
|
+
"""
|
|
37
|
+
Get the 3x3 camera intrinsics matrix K.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
[[fx, 0, cx],
|
|
41
|
+
[ 0, fy, cy],
|
|
42
|
+
[ 0, 0, 1]]
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
return np.array([[self.fx, 0.0, self.cx], [0.0, self.fy, self.cy], [0.0, 0.0, 1.0]])
|
|
46
|
+
|
|
47
|
+
def unproject_pixel(self, u: float, v: float, depth: float) -> np.ndarray:
|
|
48
|
+
"""
|
|
49
|
+
Unproject a pixel coordinate to 3D point using the camera intrinsics.
|
|
50
|
+
|
|
51
|
+
Note: This assumes no distortion. For distorted images, undistort first.
|
|
52
|
+
|
|
53
|
+
:param u: Pixel x-coordinate.
|
|
54
|
+
:param v: Pixel y-coordinate.
|
|
55
|
+
:param depth: Depth value at the pixel in meters.
|
|
56
|
+
:return: 3D point [x, y, z] in camera frame.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
x = (u - self.cx) * depth / self.fx
|
|
60
|
+
y = (v - self.cy) * depth / self.fy
|
|
61
|
+
return np.array([x, y, depth])
|
|
62
|
+
|
|
63
|
+
def project_point(self, point: np.ndarray) -> tuple[float, float]:
|
|
64
|
+
"""
|
|
65
|
+
Project a 3D point to pixel coordinates.
|
|
66
|
+
|
|
67
|
+
Note: This assumes no distortion. For accurate projection with distortion,
|
|
68
|
+
additional processing is required.
|
|
69
|
+
|
|
70
|
+
:param point: 3D point [x, y, z] in camera frame.
|
|
71
|
+
:return: Pixel coordinates (u, v).
|
|
72
|
+
:raises ValueError: If point is behind camera.
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
if point[2] <= 0:
|
|
76
|
+
raise ValueError("Point must be in front of camera (z > 0)")
|
|
77
|
+
|
|
78
|
+
u = self.fx * point[0] / point[2] + self.cx
|
|
79
|
+
v = self.fy * point[1] / point[2] + self.cy
|
|
80
|
+
return (u, v)
|
|
81
|
+
|
|
82
|
+
def is_point_visible(self, u: float, v: float) -> bool:
|
|
83
|
+
"""
|
|
84
|
+
Check if a pixel coordinate is within the image bounds.
|
|
85
|
+
|
|
86
|
+
:param u: Pixel x-coordinate.
|
|
87
|
+
:param v: Pixel y-coordinate.
|
|
88
|
+
:return: True if point is visible in the image.
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
return 0 <= u < self.width and 0 <= v < self.height
|