lcm-cli 0.1.0__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.
- lcm_cli-0.1.0.dist-info/METADATA +254 -0
- lcm_cli-0.1.0.dist-info/RECORD +23 -0
- lcm_cli-0.1.0.dist-info/WHEEL +4 -0
- lcm_cli-0.1.0.dist-info/entry_points.txt +2 -0
- lcm_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
- lcm_tools/__init__.py +3 -0
- lcm_tools/__main__.py +6 -0
- lcm_tools/cli.py +52 -0
- lcm_tools/commands/__init__.py +1 -0
- lcm_tools/commands/node_list.py +82 -0
- lcm_tools/commands/topic_echo.py +188 -0
- lcm_tools/commands/topic_list.py +69 -0
- lcm_tools/commands/topic_stats.py +87 -0
- lcm_tools/core/__init__.py +1 -0
- lcm_tools/core/discovery.py +135 -0
- lcm_tools/core/lcm_type_builder.py +577 -0
- lcm_tools/core/lcm_type_parser.py +515 -0
- lcm_tools/core/stats.py +182 -0
- lcm_tools/display/__init__.py +1 -0
- lcm_tools/display/echo_display.py +233 -0
- lcm_tools/display/stats_display.py +103 -0
- lcm_tools/listener.py +157 -0
- lcm_tools/protocol.py +172 -0
|
@@ -0,0 +1,577 @@
|
|
|
1
|
+
"""Runtime LCM type class builder.
|
|
2
|
+
|
|
3
|
+
Dynamically generates Python decode classes from parsed LcmStruct definitions.
|
|
4
|
+
Generated classes have the same interface as lcm-gen output (decode, _decode_one,
|
|
5
|
+
_get_packed_fingerprint, __slots__, etc.) and can be used directly with the
|
|
6
|
+
echo display module.
|
|
7
|
+
|
|
8
|
+
Also provides TypeRegistry for fingerprint-based auto-matching.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import struct as _struct
|
|
14
|
+
from io import BytesIO
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any, Dict, List, Optional
|
|
17
|
+
|
|
18
|
+
from lcm_tools.core.lcm_type_parser import (
|
|
19
|
+
LcmStruct,
|
|
20
|
+
compute_fingerprints,
|
|
21
|
+
parse_lcm_file,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
# ---------------------------------------------------------------------------
|
|
25
|
+
# struct format char mapping (big-endian)
|
|
26
|
+
# ---------------------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
_PRIM_FORMAT = {
|
|
29
|
+
"byte": "B",
|
|
30
|
+
"boolean": "b",
|
|
31
|
+
"int8_t": "b",
|
|
32
|
+
"int16_t": ">h",
|
|
33
|
+
"int32_t": ">i",
|
|
34
|
+
"int64_t": ">q",
|
|
35
|
+
"float": ">f",
|
|
36
|
+
"double": ">d",
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
_PRIM_SIZE = {
|
|
40
|
+
"byte": 1,
|
|
41
|
+
"boolean": 1,
|
|
42
|
+
"int8_t": 1,
|
|
43
|
+
"int16_t": 2,
|
|
44
|
+
"int32_t": 4,
|
|
45
|
+
"int64_t": 8,
|
|
46
|
+
"float": 4,
|
|
47
|
+
"double": 8,
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
# For batch unpacking of arrays (big-endian, no '>')
|
|
51
|
+
_PRIM_FMT_CHAR = {
|
|
52
|
+
"byte": "B",
|
|
53
|
+
"boolean": "b",
|
|
54
|
+
"int8_t": "b",
|
|
55
|
+
"int16_t": "h",
|
|
56
|
+
"int32_t": "i",
|
|
57
|
+
"int64_t": "q",
|
|
58
|
+
"float": "f",
|
|
59
|
+
"double": "d",
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _is_primitive(type_name: str) -> bool:
|
|
64
|
+
return type_name in _PRIM_FORMAT
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# ---------------------------------------------------------------------------
|
|
68
|
+
# Dynamic class builder
|
|
69
|
+
# ---------------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
def _build_init(members: list) -> callable:
|
|
72
|
+
"""Build __init__ method that initialises all fields to defaults."""
|
|
73
|
+
|
|
74
|
+
def __init__(self: Any) -> None:
|
|
75
|
+
for m in members:
|
|
76
|
+
type_name = m["type"]
|
|
77
|
+
dims = m["dims"]
|
|
78
|
+
|
|
79
|
+
if not dims:
|
|
80
|
+
# Scalar
|
|
81
|
+
if type_name == "byte":
|
|
82
|
+
setattr(self, m["name"], 0)
|
|
83
|
+
elif type_name == "boolean":
|
|
84
|
+
setattr(self, m["name"], False)
|
|
85
|
+
elif type_name in ("int8_t", "int16_t", "int32_t", "int64_t"):
|
|
86
|
+
setattr(self, m["name"], 0)
|
|
87
|
+
elif type_name in ("float", "double"):
|
|
88
|
+
setattr(self, m["name"], 0.0)
|
|
89
|
+
elif type_name == "string":
|
|
90
|
+
setattr(self, m["name"], "")
|
|
91
|
+
else:
|
|
92
|
+
# Compound type - will be resolved at decode time
|
|
93
|
+
setattr(self, m["name"], None)
|
|
94
|
+
else:
|
|
95
|
+
# Array
|
|
96
|
+
if dims[-1][0] == "var":
|
|
97
|
+
# Variable-length last dim -> empty list
|
|
98
|
+
setattr(self, m["name"], [])
|
|
99
|
+
else:
|
|
100
|
+
# Fixed-size: build nested lists
|
|
101
|
+
val = _build_fixed_init(type_name, dims, 0)
|
|
102
|
+
setattr(self, m["name"], val)
|
|
103
|
+
|
|
104
|
+
return __init__
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _build_fixed_init(type_name: str, dims: list, dim_idx: int) -> Any:
|
|
108
|
+
"""Recursively build fixed-size array initialiser."""
|
|
109
|
+
if dim_idx == len(dims):
|
|
110
|
+
# Leaf value
|
|
111
|
+
if type_name == "byte":
|
|
112
|
+
return 0
|
|
113
|
+
elif type_name == "boolean":
|
|
114
|
+
return False
|
|
115
|
+
elif type_name in ("int8_t", "int16_t", "int32_t", "int64_t"):
|
|
116
|
+
return 0
|
|
117
|
+
elif type_name in ("float", "double"):
|
|
118
|
+
return 0.0
|
|
119
|
+
elif type_name == "string":
|
|
120
|
+
return ""
|
|
121
|
+
else:
|
|
122
|
+
return None
|
|
123
|
+
if dim_idx == len(dims) - 1 and type_name == "byte":
|
|
124
|
+
return b""
|
|
125
|
+
size = int(dims[dim_idx][1])
|
|
126
|
+
return [_build_fixed_init(type_name, dims, dim_idx + 1) for _ in range(size)]
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _build_decode_one(member_specs: list, type_registry: "TypeRegistry") -> callable:
|
|
130
|
+
"""Build _decode_one(buf) static method (raw function, not staticmethod)."""
|
|
131
|
+
|
|
132
|
+
def _decode_one(buf: BytesIO) -> Any:
|
|
133
|
+
# 'cls' is the class itself, accessed via the closure
|
|
134
|
+
self = _decode_one._owner_cls()
|
|
135
|
+
for ms in member_specs:
|
|
136
|
+
_decode_member(self, buf, ms, type_registry)
|
|
137
|
+
return self
|
|
138
|
+
|
|
139
|
+
return _decode_one
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _decode_member(
|
|
143
|
+
self: Any, buf: BytesIO, ms: dict, registry: "TypeRegistry"
|
|
144
|
+
) -> None:
|
|
145
|
+
"""Decode a single member into self."""
|
|
146
|
+
name = ms["name"]
|
|
147
|
+
type_name = ms["type"]
|
|
148
|
+
dims = ms["dims"]
|
|
149
|
+
|
|
150
|
+
if not dims:
|
|
151
|
+
# Scalar
|
|
152
|
+
setattr(self, name, _decode_scalar(buf, type_name, registry))
|
|
153
|
+
else:
|
|
154
|
+
# Array
|
|
155
|
+
val = _decode_array(self, buf, type_name, dims, 0, registry)
|
|
156
|
+
setattr(self, name, val)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _decode_scalar(buf: BytesIO, type_name: str, registry: "TypeRegistry") -> Any:
|
|
160
|
+
"""Decode a single scalar or compound value from buf."""
|
|
161
|
+
if type_name == "string":
|
|
162
|
+
str_len = _struct.unpack(">I", buf.read(4))[0]
|
|
163
|
+
raw = buf.read(str_len)
|
|
164
|
+
return raw[:-1].decode("utf-8", "replace") if raw else ""
|
|
165
|
+
elif type_name == "boolean":
|
|
166
|
+
return bool(_struct.unpack("b", buf.read(1))[0])
|
|
167
|
+
elif type_name in _PRIM_FORMAT:
|
|
168
|
+
fmt = _PRIM_FORMAT[type_name]
|
|
169
|
+
return _struct.unpack(fmt, buf.read(_PRIM_SIZE[type_name]))[0]
|
|
170
|
+
else:
|
|
171
|
+
# Compound type
|
|
172
|
+
ref_cls = registry._classes_by_name.get(type_name)
|
|
173
|
+
if ref_cls is None:
|
|
174
|
+
raise ValueError(f"Unknown LCM type: {type_name}")
|
|
175
|
+
return ref_cls._decode_one(buf)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _resolve_dim_size(self: Any, dim: tuple) -> int:
|
|
179
|
+
"""Resolve a dimension size: const -> int, var -> self.<field>."""
|
|
180
|
+
mode, size_str = dim
|
|
181
|
+
if mode == "const":
|
|
182
|
+
return int(size_str)
|
|
183
|
+
else:
|
|
184
|
+
return getattr(self, size_str)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _decode_array(
|
|
188
|
+
self: Any,
|
|
189
|
+
buf: BytesIO,
|
|
190
|
+
type_name: str,
|
|
191
|
+
dims: list,
|
|
192
|
+
dim_idx: int,
|
|
193
|
+
registry: "TypeRegistry",
|
|
194
|
+
) -> Any:
|
|
195
|
+
"""Recursively decode a (possibly multi-dimensional) array."""
|
|
196
|
+
if dim_idx == len(dims):
|
|
197
|
+
# Should not be called here; scalar path handles this
|
|
198
|
+
return _decode_scalar(buf, type_name, registry)
|
|
199
|
+
|
|
200
|
+
dim_mode, dim_size_str = dims[dim_idx]
|
|
201
|
+
size = _resolve_dim_size(self, dims[dim_idx])
|
|
202
|
+
is_last_dim = (dim_idx == len(dims) - 1)
|
|
203
|
+
|
|
204
|
+
if is_last_dim:
|
|
205
|
+
# Last dimension: decode elements directly
|
|
206
|
+
if _is_primitive(type_name) and type_name != "string":
|
|
207
|
+
# Batch decode primitives
|
|
208
|
+
return _decode_prim_array(buf, type_name, size)
|
|
209
|
+
else:
|
|
210
|
+
# Decode elements one by one (strings or compound)
|
|
211
|
+
result = []
|
|
212
|
+
for _ in range(size):
|
|
213
|
+
result.append(_decode_scalar(buf, type_name, registry))
|
|
214
|
+
return result
|
|
215
|
+
else:
|
|
216
|
+
# Not last dimension: recurse
|
|
217
|
+
result = []
|
|
218
|
+
for _ in range(size):
|
|
219
|
+
result.append(
|
|
220
|
+
_decode_array(self, buf, type_name, dims, dim_idx + 1, registry)
|
|
221
|
+
)
|
|
222
|
+
return result
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _decode_prim_array(buf: BytesIO, type_name: str, count: int) -> Any:
|
|
226
|
+
"""Decode an array of primitive values using batch struct.unpack."""
|
|
227
|
+
if type_name == "byte":
|
|
228
|
+
return buf.read(count)
|
|
229
|
+
|
|
230
|
+
fmt_char = _PRIM_FMT_CHAR[type_name]
|
|
231
|
+
elem_size = _PRIM_SIZE[type_name]
|
|
232
|
+
raw = buf.read(count * elem_size)
|
|
233
|
+
|
|
234
|
+
if type_name == "boolean":
|
|
235
|
+
return [bool(x) for x in _struct.unpack(f">{count}{fmt_char}", raw)]
|
|
236
|
+
else:
|
|
237
|
+
return list(_struct.unpack(f">{count}{fmt_char}", raw))
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def build_lcm_class(
|
|
241
|
+
lcm_struct: LcmStruct, registry: "TypeRegistry"
|
|
242
|
+
) -> type:
|
|
243
|
+
"""Dynamically build a Python class for an LCM struct.
|
|
244
|
+
|
|
245
|
+
The generated class has the same interface as lcm-gen output:
|
|
246
|
+
- __slots__ with all member names
|
|
247
|
+
- __init__() initialising all fields
|
|
248
|
+
- decode(data) static method
|
|
249
|
+
- _decode_one(buf) static method
|
|
250
|
+
- _get_packed_fingerprint() static method
|
|
251
|
+
- _get_hash_recursive(parents) static method
|
|
252
|
+
|
|
253
|
+
Args:
|
|
254
|
+
lcm_struct: Parsed struct definition (with hash_value computed).
|
|
255
|
+
registry: TypeRegistry for resolving compound type references.
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
A dynamically created Python class.
|
|
259
|
+
"""
|
|
260
|
+
short_name = lcm_struct.short_name
|
|
261
|
+
member_names = [m.member_name for m in lcm_struct.members]
|
|
262
|
+
|
|
263
|
+
# Prepare member specs for decode
|
|
264
|
+
member_specs = []
|
|
265
|
+
for m in lcm_struct.members:
|
|
266
|
+
dims = [(d.mode, d.size) for d in m.dimensions]
|
|
267
|
+
member_specs.append({
|
|
268
|
+
"name": m.member_name,
|
|
269
|
+
"type": m.type_name,
|
|
270
|
+
"dims": dims,
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
# Build __init__
|
|
274
|
+
init_fn = _build_init(member_specs)
|
|
275
|
+
|
|
276
|
+
# Build _decode_one
|
|
277
|
+
decode_one_fn = _build_decode_one(member_specs, registry)
|
|
278
|
+
|
|
279
|
+
# Build _get_hash_recursive (raw function)
|
|
280
|
+
hash_recursive_fn = _build_hash_recursive(lcm_struct, registry)
|
|
281
|
+
|
|
282
|
+
# Build _get_packed_fingerprint (raw function)
|
|
283
|
+
packed_fp_cache: list = [None]
|
|
284
|
+
|
|
285
|
+
def _get_packed_fingerprint() -> bytes:
|
|
286
|
+
if _get_packed_fingerprint._cache[0] is None: # type: ignore[attr-defined]
|
|
287
|
+
_get_packed_fingerprint._cache[0] = _struct.pack( # type: ignore[attr-defined]
|
|
288
|
+
">Q", _get_packed_fingerprint._hash_fn([]) # type: ignore[attr-defined]
|
|
289
|
+
)
|
|
290
|
+
return _get_packed_fingerprint._cache[0] # type: ignore[attr-defined]
|
|
291
|
+
|
|
292
|
+
_get_packed_fingerprint._cache = packed_fp_cache # type: ignore[attr-defined]
|
|
293
|
+
_get_packed_fingerprint._hash_fn = hash_recursive_fn # type: ignore[attr-defined]
|
|
294
|
+
|
|
295
|
+
# Build decode (raw function)
|
|
296
|
+
def decode(data: bytes) -> Any:
|
|
297
|
+
if hasattr(data, "read"):
|
|
298
|
+
buf = data
|
|
299
|
+
else:
|
|
300
|
+
buf = BytesIO(data)
|
|
301
|
+
if buf.read(8) != decode._owner_cls._get_packed_fingerprint(): # type: ignore[attr-defined]
|
|
302
|
+
raise ValueError("Decode error: fingerprint mismatch")
|
|
303
|
+
return decode._owner_cls._decode_one(buf) # type: ignore[attr-defined]
|
|
304
|
+
|
|
305
|
+
# Build _encode_one (minimal, for encode support)
|
|
306
|
+
def _encode_one(self: Any, buf: BytesIO) -> None:
|
|
307
|
+
for ms in member_specs:
|
|
308
|
+
_encode_member(self, buf, ms, registry)
|
|
309
|
+
|
|
310
|
+
def encode(self: Any) -> bytes:
|
|
311
|
+
buf = BytesIO()
|
|
312
|
+
buf.write(self.__class__._get_packed_fingerprint())
|
|
313
|
+
self._encode_one(buf)
|
|
314
|
+
return buf.getvalue()
|
|
315
|
+
|
|
316
|
+
def get_hash(self: Any) -> int:
|
|
317
|
+
return _struct.unpack(">Q", self.__class__._get_packed_fingerprint())[0]
|
|
318
|
+
|
|
319
|
+
# Create the class
|
|
320
|
+
cls_dict: Dict[str, Any] = {
|
|
321
|
+
"__slots__": member_names,
|
|
322
|
+
"__init__": init_fn,
|
|
323
|
+
"_decode_one": staticmethod(decode_one_fn),
|
|
324
|
+
"_get_hash_recursive": staticmethod(hash_recursive_fn),
|
|
325
|
+
"_get_packed_fingerprint": staticmethod(_get_packed_fingerprint),
|
|
326
|
+
"decode": staticmethod(decode),
|
|
327
|
+
"_encode_one": _encode_one,
|
|
328
|
+
"encode": encode,
|
|
329
|
+
"get_hash": get_hash,
|
|
330
|
+
"__typenames__": [m.type_name for m in lcm_struct.members],
|
|
331
|
+
"__dimensions__": [
|
|
332
|
+
[
|
|
333
|
+
int(d.size) if d.mode == "const" else d.size
|
|
334
|
+
for d in m.dimensions
|
|
335
|
+
] if m.dimensions else None
|
|
336
|
+
for m in lcm_struct.members
|
|
337
|
+
],
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
# Add constants
|
|
341
|
+
for c in lcm_struct.constants:
|
|
342
|
+
if c.type_name in ("int8_t", "int16_t", "int32_t"):
|
|
343
|
+
cls_dict[c.name] = int(c.value_str, 0)
|
|
344
|
+
elif c.type_name == "int64_t":
|
|
345
|
+
cls_dict[c.name] = int(c.value_str, 0)
|
|
346
|
+
elif c.type_name in ("float", "double"):
|
|
347
|
+
cls_dict[c.name] = float(c.value_str)
|
|
348
|
+
|
|
349
|
+
cls = type(short_name, (object,), cls_dict)
|
|
350
|
+
|
|
351
|
+
# Store back-references so functions can find the owning class
|
|
352
|
+
decode_one_fn._owner_cls = cls # type: ignore[attr-defined]
|
|
353
|
+
decode._owner_cls = cls # type: ignore[attr-defined]
|
|
354
|
+
|
|
355
|
+
return cls
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def _build_hash_recursive(lcm_struct: LcmStruct, registry: "TypeRegistry") -> callable:
|
|
359
|
+
"""Build _get_hash_recursive(parents) matching lcm-gen output (raw function)."""
|
|
360
|
+
base_hash = lcm_struct.base_hash
|
|
361
|
+
short_name = lcm_struct.short_name
|
|
362
|
+
|
|
363
|
+
# Pre-compute which members are compound types
|
|
364
|
+
compound_members = []
|
|
365
|
+
for m in lcm_struct.members:
|
|
366
|
+
if not _is_primitive(m.type_name):
|
|
367
|
+
compound_members.append(m.type_name)
|
|
368
|
+
|
|
369
|
+
def _get_hash_recursive(parents: list) -> int:
|
|
370
|
+
if _get_hash_recursive._short_name in parents: # type: ignore[attr-defined]
|
|
371
|
+
return 0
|
|
372
|
+
newparents = parents + [_get_hash_recursive._short_name] # type: ignore[attr-defined]
|
|
373
|
+
tmphash = _get_hash_recursive._base_hash # type: ignore[attr-defined]
|
|
374
|
+
for type_name in _get_hash_recursive._compound_members: # type: ignore[attr-defined]
|
|
375
|
+
ref_cls = _get_hash_recursive._registry._classes_by_name.get(type_name) # type: ignore[attr-defined]
|
|
376
|
+
if ref_cls is not None:
|
|
377
|
+
tmphash = (tmphash + ref_cls._get_hash_recursive(newparents)) & 0xFFFFFFFFFFFFFFFF
|
|
378
|
+
tmphash = (((tmphash << 1) & 0xFFFFFFFFFFFFFFFF) + (tmphash >> 63)) & 0xFFFFFFFFFFFFFFFF
|
|
379
|
+
return tmphash
|
|
380
|
+
|
|
381
|
+
_get_hash_recursive._base_hash = base_hash # type: ignore[attr-defined]
|
|
382
|
+
_get_hash_recursive._short_name = short_name # type: ignore[attr-defined]
|
|
383
|
+
_get_hash_recursive._compound_members = compound_members # type: ignore[attr-defined]
|
|
384
|
+
_get_hash_recursive._registry = registry # type: ignore[attr-defined]
|
|
385
|
+
|
|
386
|
+
return _get_hash_recursive
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def _encode_member(self: Any, buf: BytesIO, ms: dict, registry: "TypeRegistry") -> None:
|
|
390
|
+
"""Encode a single member."""
|
|
391
|
+
name = ms["name"]
|
|
392
|
+
type_name = ms["type"]
|
|
393
|
+
dims = ms["dims"]
|
|
394
|
+
value = getattr(self, name)
|
|
395
|
+
|
|
396
|
+
if not dims:
|
|
397
|
+
_encode_scalar(buf, value, type_name, registry)
|
|
398
|
+
else:
|
|
399
|
+
_encode_array(buf, value, type_name, dims, 0, self, registry)
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def _encode_scalar(buf: BytesIO, value: Any, type_name: str, registry: "TypeRegistry") -> None:
|
|
403
|
+
if type_name == "string":
|
|
404
|
+
encoded = value.encode("utf-8")
|
|
405
|
+
buf.write(_struct.pack(">I", len(encoded) + 1))
|
|
406
|
+
buf.write(encoded)
|
|
407
|
+
buf.write(b"\x00")
|
|
408
|
+
elif type_name == "boolean":
|
|
409
|
+
buf.write(_struct.pack("b", int(value)))
|
|
410
|
+
elif type_name in _PRIM_FORMAT:
|
|
411
|
+
buf.write(_struct.pack(_PRIM_FORMAT[type_name], value))
|
|
412
|
+
else:
|
|
413
|
+
# Compound
|
|
414
|
+
value._encode_one(buf)
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def _encode_array(
|
|
418
|
+
buf: BytesIO, value: Any, type_name: str,
|
|
419
|
+
dims: list, dim_idx: int, self: Any, registry: "TypeRegistry"
|
|
420
|
+
) -> None:
|
|
421
|
+
if dim_idx == len(dims):
|
|
422
|
+
_encode_scalar(buf, value, type_name, registry)
|
|
423
|
+
return
|
|
424
|
+
|
|
425
|
+
is_last = (dim_idx == len(dims) - 1)
|
|
426
|
+
if is_last and _is_primitive(type_name) and type_name != "string":
|
|
427
|
+
_encode_prim_array(buf, value, type_name)
|
|
428
|
+
else:
|
|
429
|
+
for item in value:
|
|
430
|
+
if is_last:
|
|
431
|
+
_encode_scalar(buf, item, type_name, registry)
|
|
432
|
+
else:
|
|
433
|
+
_encode_array(buf, item, type_name, dims, dim_idx + 1, self, registry)
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def _encode_prim_array(buf: BytesIO, value: Any, type_name: str) -> None:
|
|
437
|
+
if type_name == "byte":
|
|
438
|
+
buf.write(bytearray(value))
|
|
439
|
+
return
|
|
440
|
+
fmt_char = _PRIM_FMT_CHAR[type_name]
|
|
441
|
+
count = len(value)
|
|
442
|
+
if type_name == "boolean":
|
|
443
|
+
buf.write(_struct.pack(f">{count}{fmt_char}", *[int(v) for v in value]))
|
|
444
|
+
else:
|
|
445
|
+
buf.write(_struct.pack(f">{count}{fmt_char}", *value))
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
# ---------------------------------------------------------------------------
|
|
449
|
+
# Type Registry
|
|
450
|
+
# ---------------------------------------------------------------------------
|
|
451
|
+
|
|
452
|
+
class TypeRegistry:
|
|
453
|
+
"""Registry of dynamically generated LCM decode classes.
|
|
454
|
+
|
|
455
|
+
Parses .lcm files and maintains a mapping from fingerprints and
|
|
456
|
+
type names to generated classes. Supports auto-matching by fingerprint.
|
|
457
|
+
"""
|
|
458
|
+
|
|
459
|
+
def __init__(self) -> None:
|
|
460
|
+
self._structs: List[LcmStruct] = []
|
|
461
|
+
self._classes: Dict[str, type] = {} # full_name -> class
|
|
462
|
+
self._classes_by_name: Dict[str, type] = {} # various name forms -> class
|
|
463
|
+
self._by_fingerprint: Dict[int, type] = {} # fingerprint int -> class
|
|
464
|
+
self._built = False
|
|
465
|
+
|
|
466
|
+
def register_file(self, lcm_path: str | Path) -> None:
|
|
467
|
+
"""Parse a .lcm file and register all structs found.
|
|
468
|
+
|
|
469
|
+
Also auto-discovers sibling .lcm files in the same directory to
|
|
470
|
+
resolve cross-file type references (nested struct dependencies).
|
|
471
|
+
|
|
472
|
+
Args:
|
|
473
|
+
lcm_path: Path to a .lcm file.
|
|
474
|
+
"""
|
|
475
|
+
p = Path(lcm_path)
|
|
476
|
+
structs = parse_lcm_file(p)
|
|
477
|
+
self._structs.extend(structs)
|
|
478
|
+
self._built = False
|
|
479
|
+
|
|
480
|
+
# Auto-discover sibling .lcm files for cross-file type references
|
|
481
|
+
parent = p.parent
|
|
482
|
+
if parent.is_dir():
|
|
483
|
+
registered_files = {s.source_file for s in self._structs}
|
|
484
|
+
for sibling in sorted(parent.glob("*.lcm")):
|
|
485
|
+
sibling_str = str(sibling)
|
|
486
|
+
if sibling_str not in registered_files and sibling != p:
|
|
487
|
+
try:
|
|
488
|
+
sib_structs = parse_lcm_file(sibling)
|
|
489
|
+
self._structs.extend(sib_structs)
|
|
490
|
+
except Exception:
|
|
491
|
+
pass # Skip unparseable files silently
|
|
492
|
+
|
|
493
|
+
def register_dir(self, dir_path: str | Path) -> None:
|
|
494
|
+
"""Recursively register all .lcm files in a directory.
|
|
495
|
+
|
|
496
|
+
Args:
|
|
497
|
+
dir_path: Path to a directory containing .lcm files.
|
|
498
|
+
"""
|
|
499
|
+
p = Path(dir_path)
|
|
500
|
+
if p.is_file() and p.suffix == ".lcm":
|
|
501
|
+
self.register_file(p)
|
|
502
|
+
return
|
|
503
|
+
for lcm_file in sorted(p.rglob("*.lcm")):
|
|
504
|
+
self.register_file(lcm_file)
|
|
505
|
+
|
|
506
|
+
def register_paths(self, paths: List[str | Path]) -> None:
|
|
507
|
+
"""Register multiple files/directories."""
|
|
508
|
+
for p in paths:
|
|
509
|
+
path = Path(p)
|
|
510
|
+
if path.is_dir():
|
|
511
|
+
self.register_dir(path)
|
|
512
|
+
elif path.is_file():
|
|
513
|
+
self.register_file(path)
|
|
514
|
+
else:
|
|
515
|
+
raise FileNotFoundError(f"LCM path not found: {p}")
|
|
516
|
+
|
|
517
|
+
def _build_all(self) -> None:
|
|
518
|
+
"""Compute fingerprints and build all classes."""
|
|
519
|
+
if self._built:
|
|
520
|
+
return
|
|
521
|
+
|
|
522
|
+
# Compute fingerprints across ALL registered structs
|
|
523
|
+
compute_fingerprints(self._structs)
|
|
524
|
+
|
|
525
|
+
# Build classes
|
|
526
|
+
for s in self._structs:
|
|
527
|
+
cls = build_lcm_class(s, self)
|
|
528
|
+
self._classes[s.full_name] = cls
|
|
529
|
+
|
|
530
|
+
# Register under multiple name forms for lookup
|
|
531
|
+
self._classes_by_name[s.full_name] = cls
|
|
532
|
+
if s.short_name not in self._classes_by_name:
|
|
533
|
+
self._classes_by_name[s.short_name] = cls
|
|
534
|
+
|
|
535
|
+
# Register by fingerprint
|
|
536
|
+
self._by_fingerprint[s.hash_value] = cls
|
|
537
|
+
|
|
538
|
+
self._built = True
|
|
539
|
+
|
|
540
|
+
def find_by_fingerprint(self, fp: int) -> Optional[type]:
|
|
541
|
+
"""Find a decode class by LCM fingerprint.
|
|
542
|
+
|
|
543
|
+
Args:
|
|
544
|
+
fp: The 64-bit fingerprint integer.
|
|
545
|
+
|
|
546
|
+
Returns:
|
|
547
|
+
The decode class, or None if not found.
|
|
548
|
+
"""
|
|
549
|
+
self._build_all()
|
|
550
|
+
return self._by_fingerprint.get(fp)
|
|
551
|
+
|
|
552
|
+
def find_by_name(self, name: str) -> Optional[type]:
|
|
553
|
+
"""Find a decode class by type name.
|
|
554
|
+
|
|
555
|
+
Accepts short name ("example_t"), fully-qualified ("exlcm.example_t"),
|
|
556
|
+
or module.Class format ("exlcm.example_t").
|
|
557
|
+
|
|
558
|
+
Args:
|
|
559
|
+
name: Type name to search for.
|
|
560
|
+
|
|
561
|
+
Returns:
|
|
562
|
+
The decode class, or None if not found.
|
|
563
|
+
"""
|
|
564
|
+
self._build_all()
|
|
565
|
+
return self._classes_by_name.get(name)
|
|
566
|
+
|
|
567
|
+
@property
|
|
568
|
+
def all_types(self) -> Dict[str, type]:
|
|
569
|
+
"""Return all registered classes keyed by full name."""
|
|
570
|
+
self._build_all()
|
|
571
|
+
return dict(self._classes)
|
|
572
|
+
|
|
573
|
+
@property
|
|
574
|
+
def all_fingerprints(self) -> Dict[int, str]:
|
|
575
|
+
"""Return mapping from fingerprint to type full name."""
|
|
576
|
+
self._build_all()
|
|
577
|
+
return {fp: cls.__name__ for fp, cls in self._by_fingerprint.items()}
|