telemetry-cli 1.0.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.
- telemetry_cli/__init__.py +8 -0
- telemetry_cli/_cli.py +18 -0
- telemetry_cli/_getopt.py +114 -0
- telemetry_cli/_msg.py +39 -0
- telemetry_cli/_pack.py +183 -0
- telemetry_cli/_perl.py +183 -0
- telemetry_cli/pick/__init__.py +1 -0
- telemetry_cli/pick/__main__.py +7 -0
- telemetry_cli/pick/cli.py +157 -0
- telemetry_cli/pick/commandline.py +219 -0
- telemetry_cli/pick/fields.py +166 -0
- telemetry_cli/pick/formats.py +191 -0
- telemetry_cli/pick/help.py +536 -0
- telemetry_cli/pick/symbols/airmoc +24 -0
- telemetry_cli/pick/symbols/aux +28 -0
- telemetry_cli/pick/symbols/corr +31 -0
- telemetry_cli/pick/symbols/corrOld +29 -0
- telemetry_cli/pick/symbols/hw +16 -0
- telemetry_cli/pick/symbols/moc +42 -0
- telemetry_cli/pick/symbols/subcom +83 -0
- telemetry_cli/pick/symbols/subcom_2002 +111 -0
- telemetry_cli/recl/__init__.py +1 -0
- telemetry_cli/recl/__main__.py +7 -0
- telemetry_cli/recl/cli.py +214 -0
- telemetry_cli/recl/corr.py +83 -0
- telemetry_cli/recs/__init__.py +1 -0
- telemetry_cli/recs/__main__.py +7 -0
- telemetry_cli/recs/cli.py +182 -0
- telemetry_cli/recs/extract.py +399 -0
- telemetry_cli/tgen/__init__.py +1 -0
- telemetry_cli/tgen/__main__.py +7 -0
- telemetry_cli/tgen/cli.py +128 -0
- telemetry_cli/tgen/cmdfile.py +68 -0
- telemetry_cli/tgen/convert.py +278 -0
- telemetry_cli/tgen/examples/arb_data.tgen +15 -0
- telemetry_cli/tgen/examples/calsweep.tgen +32 -0
- telemetry_cli/tgen/examples/cond_fill.tgen +17 -0
- telemetry_cli/tgen/examples/counter.tgen +11 -0
- telemetry_cli/tgen/examples/null_data.tgen +11 -0
- telemetry_cli/tgen/examples/random_size.tgen +20 -0
- telemetry_cli/tgen/examples/subcom.tgen +14 -0
- telemetry_cli/tgen/examples/valueV_value.tgen +20 -0
- telemetry_cli/tgen/runtime.py +390 -0
- telemetry_cli-1.0.0.dist-info/METADATA +106 -0
- telemetry_cli-1.0.0.dist-info/RECORD +48 -0
- telemetry_cli-1.0.0.dist-info/WHEEL +4 -0
- telemetry_cli-1.0.0.dist-info/entry_points.txt +5 -0
- telemetry_cli-1.0.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
"""Generating records from a parsed command file.
|
|
2
|
+
|
|
3
|
+
Each field of an item specification is Python code, evaluated for every
|
|
4
|
+
record in one shared namespace. A field may hold several statements
|
|
5
|
+
separated by semicolons; its value is that of the last one (an expression,
|
|
6
|
+
or the value assigned by an assignment).
|
|
7
|
+
|
|
8
|
+
The namespace provides:
|
|
9
|
+
|
|
10
|
+
I the record counter
|
|
11
|
+
F the fill byte: each read gives the fill value, or a new
|
|
12
|
+
random byte when F is set to a string containing "ran"
|
|
13
|
+
(as with the -r option)
|
|
14
|
+
X, O set by seek_file
|
|
15
|
+
DEBUG set true to trace evaluation on stderr
|
|
16
|
+
P the items: P.name.value is the item's value for this record
|
|
17
|
+
(or the last record, if it hasn't been evaluated yet), and
|
|
18
|
+
P.name._value evaluates its value field again. Likewise for
|
|
19
|
+
name, offset, trigger and type.
|
|
20
|
+
rand, srand Perl-compatible random numbers (drand48)
|
|
21
|
+
subcom_file, file_lookup, seek_file
|
|
22
|
+
read data from files
|
|
23
|
+
pmod Perl's % (on the integer parts of its operands)
|
|
24
|
+
pand, por, pxor, pshl, pshr
|
|
25
|
+
Perl's &, |, ^, << and >> (on unsigned 64-bit integers)
|
|
26
|
+
cat Perl's . (join values as text; numbers as Perl prints them)
|
|
27
|
+
sprintf Perl's sprintf
|
|
28
|
+
pi, sin, cos, tan, atan2, sqrt, exp, log, floor, ceil
|
|
29
|
+
stderr for diagnostics (stdout is the data)
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
import ast
|
|
35
|
+
import math
|
|
36
|
+
import sys
|
|
37
|
+
from collections.abc import Callable
|
|
38
|
+
from typing import BinaryIO, TextIO
|
|
39
|
+
|
|
40
|
+
from .. import _pack
|
|
41
|
+
from .._perl import UV_MAX, numify, perl_str, sprintf, to_uv
|
|
42
|
+
from .cmdfile import Item, Statement
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class TgenError(ValueError):
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class Drand48:
|
|
50
|
+
"""Perl's random number generator (Perl_drand48), so seeded runs match Perl's."""
|
|
51
|
+
|
|
52
|
+
A, C, M = 0x5DEECE66D, 0xB, 1 << 48
|
|
53
|
+
|
|
54
|
+
def __init__(self, seed: int | None = None):
|
|
55
|
+
if seed is None:
|
|
56
|
+
import os
|
|
57
|
+
|
|
58
|
+
seed = int.from_bytes(os.urandom(4), "little")
|
|
59
|
+
self.seed(seed)
|
|
60
|
+
|
|
61
|
+
def seed(self, seed: int) -> None:
|
|
62
|
+
self.x = ((int(seed) & 0xFFFFFFFF) << 16) + 0x330E
|
|
63
|
+
|
|
64
|
+
def random(self) -> float:
|
|
65
|
+
self.x = (self.A * self.x + self.C) % self.M
|
|
66
|
+
return self.x / self.M
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def perl_true(v) -> bool:
|
|
70
|
+
"""Perl's truth of a field's value: undef, 0, "" and "0" are false."""
|
|
71
|
+
if v is None:
|
|
72
|
+
return False
|
|
73
|
+
if isinstance(v, str):
|
|
74
|
+
return v not in ("", "0")
|
|
75
|
+
if isinstance(v, (list, tuple, bytes)):
|
|
76
|
+
return True # a reference, or a non-empty string of bytes
|
|
77
|
+
return bool(v)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def pmod(a, b):
|
|
81
|
+
"""Perl's a % b: both operands are truncated to integers first."""
|
|
82
|
+
a, b = int(numify(a)), int(numify(b))
|
|
83
|
+
if b == 0:
|
|
84
|
+
raise ZeroDivisionError("Illegal modulus zero")
|
|
85
|
+
return a % b
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def cat(*values) -> str:
|
|
89
|
+
"""Perl's string concatenation."""
|
|
90
|
+
return "".join(perl_str(v) for v in values)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class _FieldCode:
|
|
94
|
+
"""A compiled field: statements, then the expression whose value is the field's."""
|
|
95
|
+
|
|
96
|
+
def __init__(self, src: str, where: str):
|
|
97
|
+
self.src, self.where = src, where
|
|
98
|
+
try:
|
|
99
|
+
tree = ast.parse(src.strip() or "None", mode="exec")
|
|
100
|
+
except SyntaxError as e:
|
|
101
|
+
raise TgenError(f"{where}: can't parse {src!r}: {e.msg}") from None
|
|
102
|
+
tree = _ReadFill().visit(tree)
|
|
103
|
+
body = tree.body
|
|
104
|
+
last = body[-1] if body else ast.Expr(ast.Constant(None))
|
|
105
|
+
if isinstance(last, ast.Expr):
|
|
106
|
+
result = last.value
|
|
107
|
+
body = body[:-1]
|
|
108
|
+
elif isinstance(last, (ast.Assign, ast.AugAssign, ast.AnnAssign)):
|
|
109
|
+
target = last.targets[-1] if isinstance(last, ast.Assign) else last.target
|
|
110
|
+
result = _loaded(target)
|
|
111
|
+
else:
|
|
112
|
+
result = ast.Constant(None)
|
|
113
|
+
self.stmts = compile(ast.fix_missing_locations(ast.Module(body, [])), where, "exec")
|
|
114
|
+
self.expr = compile(ast.fix_missing_locations(ast.Expression(result)), where, "eval")
|
|
115
|
+
|
|
116
|
+
def run(self, ns: dict):
|
|
117
|
+
try:
|
|
118
|
+
exec(self.stmts, ns)
|
|
119
|
+
return eval(self.expr, ns)
|
|
120
|
+
except TgenError:
|
|
121
|
+
raise
|
|
122
|
+
except Exception as e:
|
|
123
|
+
raise TgenError(f"{self.where}: {type(e).__name__}: {e} (in {self.src!r})") from None
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _loaded(target: ast.expr) -> ast.expr:
|
|
127
|
+
"""An expression reading back an assignment target (with reads of F as reads)."""
|
|
128
|
+
node = ast.parse(ast.unparse(target), mode="eval").body
|
|
129
|
+
return _ReadFill().visit(node)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class _ReadFill(ast.NodeTransformer):
|
|
133
|
+
"""Turn each read of F into a call, so it can give a new random byte every time."""
|
|
134
|
+
|
|
135
|
+
def visit_Name(self, node: ast.Name):
|
|
136
|
+
if node.id == "F" and isinstance(node.ctx, ast.Load):
|
|
137
|
+
return ast.copy_location(ast.Call(ast.Name("_read_fill", ast.Load()), [], []), node)
|
|
138
|
+
return node
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class ItemValues:
|
|
142
|
+
"""P.name: one item's current values, and its fields for re-evaluation."""
|
|
143
|
+
|
|
144
|
+
def __init__(self, gen: Generator, item: Item):
|
|
145
|
+
self._gen, self._item = gen, item
|
|
146
|
+
self.name = item.name
|
|
147
|
+
self._current: dict = {}
|
|
148
|
+
|
|
149
|
+
def _get(self, field: str):
|
|
150
|
+
# The Perl pasted $P{name}{field} into the code as text, so a float
|
|
151
|
+
# came back with 15 significant digits.
|
|
152
|
+
v = self._current.get(field)
|
|
153
|
+
return float(perl_str(v)) if isinstance(v, float) and math.isfinite(v) else v
|
|
154
|
+
|
|
155
|
+
offset = property(lambda self: self._get("offset"))
|
|
156
|
+
trigger = property(lambda self: self._get("trigger"))
|
|
157
|
+
type = property(lambda self: self._get("type"))
|
|
158
|
+
value = property(lambda self: self._get("value"))
|
|
159
|
+
|
|
160
|
+
@property
|
|
161
|
+
def _name(self):
|
|
162
|
+
return self._item.name
|
|
163
|
+
|
|
164
|
+
@property
|
|
165
|
+
def _offset(self):
|
|
166
|
+
return self._gen._code(self._item, "offset").run(self._gen.ns)
|
|
167
|
+
|
|
168
|
+
@property
|
|
169
|
+
def _trigger(self):
|
|
170
|
+
return self._gen._code(self._item, "trigger").run(self._gen.ns)
|
|
171
|
+
|
|
172
|
+
@property
|
|
173
|
+
def _type(self):
|
|
174
|
+
return self._gen._code(self._item, "type").run(self._gen.ns)
|
|
175
|
+
|
|
176
|
+
@property
|
|
177
|
+
def _value(self):
|
|
178
|
+
return self._gen._code(self._item, "value").run(self._gen.ns)
|
|
179
|
+
|
|
180
|
+
def __getitem__(self, key: str):
|
|
181
|
+
return getattr(self, key)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class Items:
|
|
185
|
+
def __init__(self):
|
|
186
|
+
self._items: dict[str, ItemValues] = {}
|
|
187
|
+
|
|
188
|
+
def __getattr__(self, name: str) -> ItemValues:
|
|
189
|
+
try:
|
|
190
|
+
return self.__dict__["_items"][name]
|
|
191
|
+
except KeyError:
|
|
192
|
+
raise AttributeError(f"no item named {name!r}") from None
|
|
193
|
+
|
|
194
|
+
def __getitem__(self, name: str) -> ItemValues:
|
|
195
|
+
return self._items[name]
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
class Generator:
|
|
199
|
+
def __init__(
|
|
200
|
+
self,
|
|
201
|
+
items: list[Item],
|
|
202
|
+
statements: list[Statement],
|
|
203
|
+
*,
|
|
204
|
+
fill="0",
|
|
205
|
+
seed: int | None = None,
|
|
206
|
+
stderr: TextIO | None = None,
|
|
207
|
+
source: str = "command file",
|
|
208
|
+
):
|
|
209
|
+
self.items = items
|
|
210
|
+
self.source = source
|
|
211
|
+
self.stderr = stderr if stderr is not None else sys.stderr
|
|
212
|
+
self.rng = Drand48(seed)
|
|
213
|
+
self.files: dict[str, BinaryIO] = {}
|
|
214
|
+
self._compiled: dict[tuple[int, str], _FieldCode] = {}
|
|
215
|
+
self.P = Items()
|
|
216
|
+
for it in items:
|
|
217
|
+
self.P._items[it.name] = ItemValues(self, it)
|
|
218
|
+
self.ns: dict = self._namespace(fill)
|
|
219
|
+
for st in statements:
|
|
220
|
+
_FieldCode(st.code, f"{source}:{st.line}").run(self.ns)
|
|
221
|
+
|
|
222
|
+
# --- the namespace --------------------------------------------------------
|
|
223
|
+
|
|
224
|
+
def _namespace(self, fill) -> dict:
|
|
225
|
+
ns = {
|
|
226
|
+
"I": 0,
|
|
227
|
+
"F": fill,
|
|
228
|
+
"X": None,
|
|
229
|
+
"O": 0,
|
|
230
|
+
"DEBUG": 0,
|
|
231
|
+
"P": self.P,
|
|
232
|
+
"rand": self.rand,
|
|
233
|
+
"srand": self.srand,
|
|
234
|
+
"subcom_file": self.subcom_file,
|
|
235
|
+
"file_lookup": self.file_lookup,
|
|
236
|
+
"seek_file": self.seek_file,
|
|
237
|
+
"pmod": pmod,
|
|
238
|
+
"pand": lambda a, b: to_uv(a) & to_uv(b),
|
|
239
|
+
"por": lambda a, b: to_uv(a) | to_uv(b),
|
|
240
|
+
"pxor": lambda a, b: to_uv(a) ^ to_uv(b),
|
|
241
|
+
"pshl": lambda a, b: (to_uv(a) << to_uv(b)) & UV_MAX if to_uv(b) < 64 else 0,
|
|
242
|
+
"pshr": lambda a, b: to_uv(a) >> to_uv(b) if to_uv(b) < 64 else 0,
|
|
243
|
+
"cat": cat,
|
|
244
|
+
"sprintf": sprintf,
|
|
245
|
+
"stderr": self.stderr,
|
|
246
|
+
"_read_fill": self.read_fill,
|
|
247
|
+
}
|
|
248
|
+
for name in ("pi", "sin", "cos", "tan", "atan2", "sqrt", "exp", "log", "floor", "ceil"):
|
|
249
|
+
ns[name] = getattr(math, name)
|
|
250
|
+
return ns
|
|
251
|
+
|
|
252
|
+
def rand(self, n=1):
|
|
253
|
+
"""Perl's rand: a float in [0, n); rand(0) is rand(1)."""
|
|
254
|
+
n = numify(n) or 1
|
|
255
|
+
return self.rng.random() * n
|
|
256
|
+
|
|
257
|
+
def srand(self, seed=0):
|
|
258
|
+
self.rng.seed(int(numify(seed)))
|
|
259
|
+
return seed
|
|
260
|
+
|
|
261
|
+
def read_fill(self):
|
|
262
|
+
"""The value of F: the fill, or a random byte if the fill is "random"."""
|
|
263
|
+
mode = self.ns["F"]
|
|
264
|
+
if isinstance(mode, str) and "ran" in mode:
|
|
265
|
+
return int(self.rng.random() * 256)
|
|
266
|
+
return mode
|
|
267
|
+
|
|
268
|
+
# --- file helpers (each file is opened once) ------------------------------
|
|
269
|
+
|
|
270
|
+
def _file(self, name) -> BinaryIO:
|
|
271
|
+
name = str(name)
|
|
272
|
+
if name not in self.files:
|
|
273
|
+
try:
|
|
274
|
+
self.files[name] = open(name, "rb") # noqa: SIM115
|
|
275
|
+
except OSError as e:
|
|
276
|
+
raise TgenError(f"Can't read from {name}: {e.strerror}") from None
|
|
277
|
+
return self.files[name]
|
|
278
|
+
|
|
279
|
+
def _read(self, name, offset, nbytes) -> bytes | None:
|
|
280
|
+
f = self._file(name)
|
|
281
|
+
offset = int(numify(offset))
|
|
282
|
+
if offset < 0:
|
|
283
|
+
return None
|
|
284
|
+
f.seek(offset)
|
|
285
|
+
return f.read(max(int(numify(nbytes)), 0))
|
|
286
|
+
|
|
287
|
+
def subcom_file(self, file, offset, nbytes):
|
|
288
|
+
"""nbytes of file starting at offset, as a byte string."""
|
|
289
|
+
data = self._read(file, offset, nbytes)
|
|
290
|
+
return b"" if data is None else data
|
|
291
|
+
|
|
292
|
+
def file_lookup(self, file, offset, nbytes, unpack_type):
|
|
293
|
+
"""The first value unpacked from nbytes of file at offset (0 if they can't be read)."""
|
|
294
|
+
nbytes = int(numify(nbytes))
|
|
295
|
+
data = self._read(file, offset, nbytes)
|
|
296
|
+
if data is None or len(data) != nbytes:
|
|
297
|
+
return 0
|
|
298
|
+
values = _pack.unpack(str(unpack_type), data)
|
|
299
|
+
return values[0] if values else None
|
|
300
|
+
|
|
301
|
+
def seek_file(self, file, offset, nbytes, unpack_type, cond):
|
|
302
|
+
"""Read nbytes at offset (evaluated with O = 0, 1, 2, ...) until cond is true.
|
|
303
|
+
|
|
304
|
+
offset and cond are code, as strings; X holds the value just read.
|
|
305
|
+
Returns the offset that satisfied cond, or -1 at the end of the file.
|
|
306
|
+
"""
|
|
307
|
+
offset_code = _FieldCode(str(offset), "seek_file offset")
|
|
308
|
+
cond_code = _FieldCode(str(cond), "seek_file condition")
|
|
309
|
+
nbytes = int(numify(nbytes))
|
|
310
|
+
self.ns["O"] = 0
|
|
311
|
+
while True:
|
|
312
|
+
at = offset_code.run(self.ns)
|
|
313
|
+
data = self._read(file, at, nbytes)
|
|
314
|
+
if data is None or len(data) != nbytes:
|
|
315
|
+
return -1
|
|
316
|
+
values = _pack.unpack(str(unpack_type), data)
|
|
317
|
+
self.ns["X"] = values[0] if values else None
|
|
318
|
+
if self.ns.get("DEBUG"):
|
|
319
|
+
print(f"o={self.ns['O']}; seek_value={self.ns['X']}", file=self.stderr)
|
|
320
|
+
self.ns["O"] += 1
|
|
321
|
+
if perl_true(cond_code.run(self.ns)):
|
|
322
|
+
return at
|
|
323
|
+
|
|
324
|
+
# --- generating -------------------------------------------------------------
|
|
325
|
+
|
|
326
|
+
def _code(self, item: Item, field: str) -> _FieldCode:
|
|
327
|
+
key = (id(item), field)
|
|
328
|
+
if key not in self._compiled:
|
|
329
|
+
self._compiled[key] = _FieldCode(
|
|
330
|
+
getattr(item, field), f"{self.source}:{item.line} ({item.name}, {field})"
|
|
331
|
+
)
|
|
332
|
+
return self._compiled[key]
|
|
333
|
+
|
|
334
|
+
def _eval(self, item: Item, field: str):
|
|
335
|
+
value = self._code(item, field).run(self.ns)
|
|
336
|
+
if self.ns.get("DEBUG"):
|
|
337
|
+
print(f"{item.name}.{field}: {getattr(item, field)!r} -> {value!r}", file=self.stderr)
|
|
338
|
+
return value
|
|
339
|
+
|
|
340
|
+
def frame(self) -> bytes:
|
|
341
|
+
frame: list[int | None] = []
|
|
342
|
+
for item in self.items:
|
|
343
|
+
cur = self.P._items[item.name]._current
|
|
344
|
+
cur["offset"] = offset = self._eval(item, "offset")
|
|
345
|
+
cur["trigger"] = trigger = self._eval(item, "trigger")
|
|
346
|
+
if not perl_true(trigger):
|
|
347
|
+
continue
|
|
348
|
+
if numify(offset) < 0: # a calculation only: evaluate the value, don't pack
|
|
349
|
+
cur["value"] = self._eval(item, "value")
|
|
350
|
+
continue
|
|
351
|
+
cur["type"] = type_ = self._eval(item, "type")
|
|
352
|
+
cur["value"] = value = self._eval(item, "value")
|
|
353
|
+
values = list(value) if isinstance(value, (list, tuple)) else [value]
|
|
354
|
+
try:
|
|
355
|
+
packed = _pack.pack(str(type_), *values)
|
|
356
|
+
except _pack.PackError as e:
|
|
357
|
+
raise TgenError(f"{self.source}:{item.line} ({item.name}): {e}") from None
|
|
358
|
+
start = int(numify(offset))
|
|
359
|
+
end = start + len(packed)
|
|
360
|
+
if end > len(frame):
|
|
361
|
+
frame.extend([None] * (end - len(frame)))
|
|
362
|
+
frame[start:end] = packed
|
|
363
|
+
out = bytearray()
|
|
364
|
+
for b in frame:
|
|
365
|
+
if b is None:
|
|
366
|
+
out.append(int(numify(self.read_fill())) & 0xFF)
|
|
367
|
+
else:
|
|
368
|
+
out.append(b)
|
|
369
|
+
return bytes(out)
|
|
370
|
+
|
|
371
|
+
def run(
|
|
372
|
+
self,
|
|
373
|
+
out: BinaryIO,
|
|
374
|
+
nframes: int,
|
|
375
|
+
first: int,
|
|
376
|
+
*,
|
|
377
|
+
notify: int = 100,
|
|
378
|
+
quiet: bool = False,
|
|
379
|
+
progress: Callable[[str], None] | None = None,
|
|
380
|
+
) -> None:
|
|
381
|
+
last = first + nframes - 1
|
|
382
|
+
self.ns["I"] = first
|
|
383
|
+
while numify(self.ns["I"]) <= last:
|
|
384
|
+
out.write(self.frame())
|
|
385
|
+
i = int(numify(self.ns["I"]))
|
|
386
|
+
if not quiet and not (i + 1) % notify and progress:
|
|
387
|
+
progress(f"{i}/{last}")
|
|
388
|
+
self.ns["I"] = self.ns["I"] + 1
|
|
389
|
+
for f in self.files.values():
|
|
390
|
+
f.close()
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: telemetry-cli
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Command-line tools for binary telemetry: decode, extract, measure and generate records
|
|
5
|
+
Project-URL: Repository, https://github.com/donalgrant/telemetry-cli
|
|
6
|
+
Project-URL: Documentation, https://github.com/donalgrant/telemetry-cli/tree/main/docs
|
|
7
|
+
Project-URL: Changelog, https://github.com/donalgrant/telemetry-cli/blob/main/CHANGELOG.md
|
|
8
|
+
Project-URL: Issues, https://github.com/donalgrant/telemetry-cli/issues
|
|
9
|
+
Author: David A. Imel
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: binary,cli,decoding,pcm,records,telemetry
|
|
13
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
14
|
+
Classifier: Environment :: Console
|
|
15
|
+
Classifier: Intended Audience :: Science/Research
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Topic :: Scientific/Engineering
|
|
23
|
+
Classifier: Topic :: Utilities
|
|
24
|
+
Requires-Python: >=3.10
|
|
25
|
+
Requires-Dist: numpy>=1.23
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: hypothesis>=6; extra == 'dev'
|
|
28
|
+
Requires-Dist: pytest-cov>=4; extra == 'dev'
|
|
29
|
+
Requires-Dist: pytest>=7; extra == 'dev'
|
|
30
|
+
Requires-Dist: ruff>=0.5; extra == 'dev'
|
|
31
|
+
Requires-Dist: tomli>=2; (python_version < '3.11') and extra == 'dev'
|
|
32
|
+
Description-Content-Type: text/markdown
|
|
33
|
+
|
|
34
|
+
# telemetry-cli
|
|
35
|
+
|
|
36
|
+
Command-line tools for binary telemetry, built for Unix pipelines:
|
|
37
|
+
|
|
38
|
+
- **pick** decodes fields from fixed-length binary records: integers, floats,
|
|
39
|
+
complex numbers, characters and bit fields, in any byte order
|
|
40
|
+
([manual](https://github.com/donalgrant/telemetry-cli/blob/main/docs/pick.md)).
|
|
41
|
+
- **recs** extracts records from a byte stream by their start (and end)
|
|
42
|
+
markers ([manual](https://github.com/donalgrant/telemetry-cli/blob/main/docs/recs.md)).
|
|
43
|
+
- **recl** estimates the record length of a binary file
|
|
44
|
+
([manual](https://github.com/donalgrant/telemetry-cli/blob/main/docs/recl.md)).
|
|
45
|
+
- **tgen** generates synthetic telemetry from a command file describing each
|
|
46
|
+
record ([manual](https://github.com/donalgrant/telemetry-cli/blob/main/docs/tgen.md)).
|
|
47
|
+
|
|
48
|
+
```console
|
|
49
|
+
$ printf '\x00\x00\x00\x01\xff\xfeHi\x00\x00\x00\x02\x00\x07ok' > demo.bin
|
|
50
|
+
$ pick demo.bin 8 -q order=big u s 2S
|
|
51
|
+
1 -2 Hi
|
|
52
|
+
2 7 ok
|
|
53
|
+
$ pick demo.bin 8 -q order=big -n u0x w2b
|
|
54
|
+
0 00 00 00 01 1111 1111 1111 1110
|
|
55
|
+
1 00 00 00 02 0000 0000 0000 0111
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Together: tgen makes a stream, recs pulls the frames out of it, recl finds
|
|
59
|
+
their length, and pick decodes them. See
|
|
60
|
+
[Using the tools together](https://github.com/donalgrant/telemetry-cli/blob/main/docs/pipelines.md).
|
|
61
|
+
|
|
62
|
+
## Install
|
|
63
|
+
|
|
64
|
+
Requires Python 3.10 or later. The package is `telemetry-cli` on PyPI, and is
|
|
65
|
+
best installed as an isolated command-line tool:
|
|
66
|
+
|
|
67
|
+
```text
|
|
68
|
+
pipx install telemetry-cli
|
|
69
|
+
# or
|
|
70
|
+
uv tool install telemetry-cli
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Either installs the commands `pick`, `recs`, `recl` and `tgen`. You can also
|
|
74
|
+
`pip install telemetry-cli` in a virtual environment.
|
|
75
|
+
|
|
76
|
+
## History
|
|
77
|
+
|
|
78
|
+
These are Python ports of tools written at JPL. pick and recl began together
|
|
79
|
+
as C++ tools, for a project whose telemetry arrived with no documentation of
|
|
80
|
+
its structure: recl to find the length of the records, and pick to take them
|
|
81
|
+
apart and work out what was in them. (A bit reminiscent of the film
|
|
82
|
+
*Contact*.) Both were later rewritten in Perl (pick by 2000), and tgen (2003)
|
|
83
|
+
and recs joined them.
|
|
84
|
+
|
|
85
|
+
The command lines are the same, and so is the output, apart from fixes listed
|
|
86
|
+
in the [CHANGELOG](https://github.com/donalgrant/telemetry-cli/blob/main/CHANGELOG.md):
|
|
87
|
+
chiefly pick's byte-order handling on little-endian machines, and recl's
|
|
88
|
+
measurement. tgen's command files are now Python; `tgen --convert` translates
|
|
89
|
+
the old Perl ones. The Perl originals, with their history, are in
|
|
90
|
+
[legacy/](https://github.com/donalgrant/telemetry-cli/tree/main/legacy).
|
|
91
|
+
|
|
92
|
+
## Development
|
|
93
|
+
|
|
94
|
+
```text
|
|
95
|
+
python -m venv .venv && .venv/bin/pip install -e '.[dev]'
|
|
96
|
+
.venv/bin/pytest
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
The tests compare each tool with its Perl original. `tests/golden/` holds the
|
|
100
|
+
Perl's output for large tables of command lines (about 2,200 for pick, 700
|
|
101
|
+
for recs), and the tests check that the Python version reproduces it byte for
|
|
102
|
+
byte. Where it deliberately differs, the case names the CHANGELOG entry that
|
|
103
|
+
explains why. With perl installed, further tests run random command lines
|
|
104
|
+
through both versions and compare them (`HYPOTHESIS_PROFILE=deep pytest` for
|
|
105
|
+
a longer search), and `python tools/make_golden.py --check` confirms the saved
|
|
106
|
+
output is current. The examples in the documentation run as tests too.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
telemetry_cli/__init__.py,sha256=eb6NP89vHLTizc4DECIfkXYph1MHAVG3lP6xRh9PZDw,281
|
|
2
|
+
telemetry_cli/_cli.py,sha256=Toez71kX-dZeAlgN1WxqujmG2htv_FXHrWeGjVJLH-U,508
|
|
3
|
+
telemetry_cli/_getopt.py,sha256=v8DxS0bz0pbGcYqZTebQEle0Xkji-Ue8CE0KM9ixKUs,3800
|
|
4
|
+
telemetry_cli/_msg.py,sha256=HEpnPAtRBIpA41DULrwMcMlJGVzNV9LM9AoJ7vqRZvM,1239
|
|
5
|
+
telemetry_cli/_pack.py,sha256=DkoGgN_15gntl3GMZkiVl2GkiHyVGTljQOnaT6diLSY,6205
|
|
6
|
+
telemetry_cli/_perl.py,sha256=VV7g_48MCtnAvmmpgGYJjsvXUhDsvmPjQ7tdt1JsmGI,5841
|
|
7
|
+
telemetry_cli/pick/__init__.py,sha256=tf-fMfkUVpW4nhWQu6D8ctDkl3o8-9AIc-yM2UvKs60,71
|
|
8
|
+
telemetry_cli/pick/__main__.py,sha256=R3wrnWRPRBDHCRIznlmRsnZ-nnDdJN7Lgo-Gnj8PgAE,99
|
|
9
|
+
telemetry_cli/pick/cli.py,sha256=sGWx6I-e-GZa7tNrzsNtR2HK_XKfk21PEh0GXMP1suQ,5178
|
|
10
|
+
telemetry_cli/pick/commandline.py,sha256=4rE0ADiJ0yeSN-WFQozy-PegkQPAtMPVOTXwLjDEy78,7725
|
|
11
|
+
telemetry_cli/pick/fields.py,sha256=UtZoAotrkYSWP37SRjslysesuVelu8pXVH9iUT1clHM,5842
|
|
12
|
+
telemetry_cli/pick/formats.py,sha256=rveagoksyngGNJgTvMm3lDM6A-3X3fH20K0tjzfeCTk,6908
|
|
13
|
+
telemetry_cli/pick/help.py,sha256=Iie7L72JDWnNedb9dogOi02VcZd6AJqzPgsfk2tqeYI,18548
|
|
14
|
+
telemetry_cli/pick/symbols/airmoc,sha256=vHkLQrkCCGhYDaPsKiAzoz5IfUXXiNSZ7GRW-y5MbGI,366
|
|
15
|
+
telemetry_cli/pick/symbols/aux,sha256=wZ4rdl5XJiPL3ojuHiA3rmYaYkfgoBuNT1Bd-JxjBRc,409
|
|
16
|
+
telemetry_cli/pick/symbols/corr,sha256=MCAFjE-hbWVwgj1LwFnOSLRDZWoeSNPFXiwDc8RkXCg,623
|
|
17
|
+
telemetry_cli/pick/symbols/corrOld,sha256=E3fF8GL8XNJsoHfE1Z6KfXyyXuuc40du5YbCKC-YhbU,599
|
|
18
|
+
telemetry_cli/pick/symbols/hw,sha256=statq5EeYbWwOXy4EC8BzveTyhGaxxVydJyMTbF4ltE,190
|
|
19
|
+
telemetry_cli/pick/symbols/moc,sha256=MFj0nYy-o-YDa8C8iaztFASj4rqCterlVKAIJtJhEVs,664
|
|
20
|
+
telemetry_cli/pick/symbols/subcom,sha256=GxSFJjxwZSeg5LlXyNVRS1vW0Q2ZD5uPkhKMp_1ERq8,2446
|
|
21
|
+
telemetry_cli/pick/symbols/subcom_2002,sha256=sAjEgc6qwwLXwXzd9aq1wXQnMEGKXI1juBYO4A8WH98,2823
|
|
22
|
+
telemetry_cli/recl/__init__.py,sha256=z4QFTEsk7RwgQHQCwjODqE5YSO0AnE30i0REjhAAX2A,57
|
|
23
|
+
telemetry_cli/recl/__main__.py,sha256=30MLg92M0QF-x99qm1OapckDppgFkSgurwxg_Ne8GTI,99
|
|
24
|
+
telemetry_cli/recl/cli.py,sha256=y2xdUzbHojbrT_AEacmPjB3S0hFs4wUnyd7cdBalMoc,7257
|
|
25
|
+
telemetry_cli/recl/corr.py,sha256=z_fxBFrBw0gILJ8-7ZWQgYDSJWcFKvti4RsX28jxtFw,3536
|
|
26
|
+
telemetry_cli/recs/__init__.py,sha256=E_nQYAX3sjrMia2Wk-x_HOZIgIWcWarJE3km7XGGLXw,82
|
|
27
|
+
telemetry_cli/recs/__main__.py,sha256=KwNEOVw9Po2oqwSLDAqwyq7S2XSfMTGOoWNLs3vO4zE,99
|
|
28
|
+
telemetry_cli/recs/cli.py,sha256=flmY1LRg-JRnagMAVyRCLLuvvWmUpUtjfYGXeJQIbIo,5886
|
|
29
|
+
telemetry_cli/recs/extract.py,sha256=ggGsB5c6wfHaC6sjuC0Npv1csr9dFZIMoLs1NSOnptg,13854
|
|
30
|
+
telemetry_cli/tgen/__init__.py,sha256=1mvMJA14vBGpCXT07p07xuY8nQAS-ulJZCJXdyXYrJY,60
|
|
31
|
+
telemetry_cli/tgen/__main__.py,sha256=os93C6HnHnoAE5JGHRQHog0Z0Ai76-LpU9O8Zu5_w-s,99
|
|
32
|
+
telemetry_cli/tgen/cli.py,sha256=csAJMRESnff07BpXGnXo9PIe9zSCquovo-5cs8132CU,4198
|
|
33
|
+
telemetry_cli/tgen/cmdfile.py,sha256=07iLI_Ldm9MNLerAF4mPMr-1h2cr6-SgXBcu3314vMY,1725
|
|
34
|
+
telemetry_cli/tgen/convert.py,sha256=x_BhnLNZLih1PX5QxKPWht1wDiLhmkEtQZdF5vV-mCE,9479
|
|
35
|
+
telemetry_cli/tgen/runtime.py,sha256=Q0bHzqDeTP7-IypT85AOimvPnDW7WvWJs78QWZwxk94,13959
|
|
36
|
+
telemetry_cli/tgen/examples/arb_data.tgen,sha256=ZV_EHPJWEzzwd5U6l4_3JZxtZvedBy8XyY4x2_hAjJ0,554
|
|
37
|
+
telemetry_cli/tgen/examples/calsweep.tgen,sha256=zG29oFJ3ieTT-h4yi1HA4uMAElhpmi6b8P8JWtnYGuU,955
|
|
38
|
+
telemetry_cli/tgen/examples/cond_fill.tgen,sha256=ddShQz4dn40xwfpCra6h3iag6aBmrJAOakN7N2tqjAw,487
|
|
39
|
+
telemetry_cli/tgen/examples/counter.tgen,sha256=5LoP-S2hbGUfNdcoHsktCSTZNiro8vvT8QTGIp1LnoI,262
|
|
40
|
+
telemetry_cli/tgen/examples/null_data.tgen,sha256=GKomAS3eyyBf_AXm5wezKdV-98QN-1k80lpFL4fWIhk,319
|
|
41
|
+
telemetry_cli/tgen/examples/random_size.tgen,sha256=9t_6ir51jVbjKM8TR6cw6qWUWyyY6gQhEKicnM1ENUs,655
|
|
42
|
+
telemetry_cli/tgen/examples/subcom.tgen,sha256=Hw3OACp9ueA4hsQimYT4R3w6U8DvAhd3zMlq0O_YvYM,436
|
|
43
|
+
telemetry_cli/tgen/examples/valueV_value.tgen,sha256=ZnFGtF-trqeEHowmTghYW8JNR0SXMnOovXVXyL-Vdcg,674
|
|
44
|
+
telemetry_cli-1.0.0.dist-info/METADATA,sha256=PT8DdJr04W51n4jQErEuBh51tclxgjr53AnuEfc-pas,4640
|
|
45
|
+
telemetry_cli-1.0.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
46
|
+
telemetry_cli-1.0.0.dist-info/entry_points.txt,sha256=sOtbf_d1OIyGvm9DPBSUddoTjWZmMXePZNl69TzqZKA,158
|
|
47
|
+
telemetry_cli-1.0.0.dist-info/licenses/LICENSE,sha256=PiTqw42Wp1U8DisYmUyCMgJny4s3Hd-0_hMWUciTX-k,1075
|
|
48
|
+
telemetry_cli-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2000-2026 David A. Imel
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|