oscura 0.3.0__py3-none-any.whl → 0.5.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.
- oscura/__init__.py +1 -7
- oscura/acquisition/__init__.py +147 -0
- oscura/acquisition/file.py +255 -0
- oscura/acquisition/hardware.py +186 -0
- oscura/acquisition/saleae.py +340 -0
- oscura/acquisition/socketcan.py +315 -0
- oscura/acquisition/streaming.py +38 -0
- oscura/acquisition/synthetic.py +229 -0
- oscura/acquisition/visa.py +376 -0
- oscura/analyzers/__init__.py +3 -0
- oscura/analyzers/digital/__init__.py +48 -0
- oscura/analyzers/digital/clock.py +9 -1
- oscura/analyzers/digital/edges.py +1 -1
- oscura/analyzers/digital/extraction.py +195 -0
- oscura/analyzers/digital/ic_database.py +498 -0
- oscura/analyzers/digital/timing.py +41 -11
- oscura/analyzers/digital/timing_paths.py +339 -0
- oscura/analyzers/digital/vintage.py +377 -0
- oscura/analyzers/digital/vintage_result.py +148 -0
- oscura/analyzers/protocols/__init__.py +22 -1
- oscura/analyzers/protocols/parallel_bus.py +449 -0
- oscura/analyzers/side_channel/__init__.py +52 -0
- oscura/analyzers/side_channel/power.py +690 -0
- oscura/analyzers/side_channel/timing.py +369 -0
- oscura/analyzers/signal_integrity/sparams.py +1 -1
- oscura/automotive/__init__.py +4 -2
- oscura/automotive/can/patterns.py +3 -1
- oscura/automotive/can/session.py +277 -78
- oscura/automotive/can/state_machine.py +5 -2
- oscura/builders/__init__.py +9 -11
- oscura/builders/signal_builder.py +99 -191
- oscura/core/exceptions.py +5 -1
- oscura/export/__init__.py +12 -0
- oscura/export/wavedrom.py +430 -0
- oscura/exporters/json_export.py +47 -0
- oscura/exporters/vintage_logic_csv.py +247 -0
- oscura/loaders/__init__.py +1 -0
- oscura/loaders/chipwhisperer.py +393 -0
- oscura/loaders/touchstone.py +1 -1
- oscura/reporting/__init__.py +7 -0
- oscura/reporting/vintage_logic_report.py +523 -0
- oscura/session/session.py +54 -46
- oscura/sessions/__init__.py +70 -0
- oscura/sessions/base.py +323 -0
- oscura/sessions/blackbox.py +640 -0
- oscura/sessions/generic.py +189 -0
- oscura/utils/autodetect.py +5 -1
- oscura/visualization/digital_advanced.py +718 -0
- oscura/visualization/figure_manager.py +156 -0
- {oscura-0.3.0.dist-info → oscura-0.5.0.dist-info}/METADATA +86 -5
- {oscura-0.3.0.dist-info → oscura-0.5.0.dist-info}/RECORD +54 -33
- oscura/automotive/dtc/data.json +0 -2763
- oscura/schemas/bus_configuration.json +0 -322
- oscura/schemas/device_mapping.json +0 -182
- oscura/schemas/packet_format.json +0 -418
- oscura/schemas/protocol_definition.json +0 -363
- {oscura-0.3.0.dist-info → oscura-0.5.0.dist-info}/WHEEL +0 -0
- {oscura-0.3.0.dist-info → oscura-0.5.0.dist-info}/entry_points.txt +0 -0
- {oscura-0.3.0.dist-info → oscura-0.5.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
"""WaveDrom timing diagram generation.
|
|
2
|
+
|
|
3
|
+
Creates WaveDrom JSON format timing diagrams from digital signals.
|
|
4
|
+
WaveDrom format can be rendered as SVG/PNG using wavedrom-cli or online tools.
|
|
5
|
+
|
|
6
|
+
Example:
|
|
7
|
+
>>> from oscura.export.wavedrom import export_wavedrom, WaveDromBuilder
|
|
8
|
+
>>> builder = WaveDromBuilder()
|
|
9
|
+
>>> builder.add_clock("CLK", period=100e-9)
|
|
10
|
+
>>> builder.add_signal("DATA", edges=[10e-9, 50e-9, 150e-9])
|
|
11
|
+
>>> builder.add_arrow(10e-9, 40e-9, "t_su = 30ns")
|
|
12
|
+
>>> json_output = builder.to_json()
|
|
13
|
+
>>> export_wavedrom(json_output, "timing_diagram.json")
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import json
|
|
19
|
+
from dataclasses import dataclass
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import TYPE_CHECKING, Any, Literal
|
|
22
|
+
|
|
23
|
+
import numpy as np
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from oscura.core.types import DigitalTrace, WaveformTrace
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class WaveDromSignal:
|
|
31
|
+
"""A WaveDrom signal definition.
|
|
32
|
+
|
|
33
|
+
Attributes:
|
|
34
|
+
name: Signal name.
|
|
35
|
+
wave: Wave string (WaveDrom format).
|
|
36
|
+
data: Optional data labels.
|
|
37
|
+
node: Optional node markers for arrows.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
name: str
|
|
41
|
+
wave: str
|
|
42
|
+
data: list[str] | None = None
|
|
43
|
+
node: str | None = None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass
|
|
47
|
+
class WaveDromEdge:
|
|
48
|
+
"""A WaveDrom edge/arrow annotation.
|
|
49
|
+
|
|
50
|
+
Attributes:
|
|
51
|
+
from_node: Source node name.
|
|
52
|
+
to_node: Destination node name.
|
|
53
|
+
label: Arrow label text.
|
|
54
|
+
style: Arrow style.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
from_node: str
|
|
58
|
+
to_node: str
|
|
59
|
+
label: str
|
|
60
|
+
style: str = ""
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class WaveDromBuilder:
|
|
64
|
+
"""Builder for WaveDrom timing diagrams.
|
|
65
|
+
|
|
66
|
+
Example:
|
|
67
|
+
>>> builder = WaveDromBuilder(title="74LS74 Setup Time")
|
|
68
|
+
>>> builder.add_clock("CLK", period=100e-9, start_time=0)
|
|
69
|
+
>>> builder.add_signal("D", edges=[10e-9, 50e-9])
|
|
70
|
+
>>> builder.add_arrow(10e-9, 40e-9, "t_su = 30ns")
|
|
71
|
+
>>> json_str = builder.to_json()
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
def __init__(
|
|
75
|
+
self,
|
|
76
|
+
*,
|
|
77
|
+
title: str | None = None,
|
|
78
|
+
time_scale: float = 1e-9, # Default to nanoseconds
|
|
79
|
+
):
|
|
80
|
+
"""Initialize WaveDrom builder.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
title: Optional diagram title.
|
|
84
|
+
time_scale: Time scale for signal conversion (default 1ns).
|
|
85
|
+
"""
|
|
86
|
+
self.title = title
|
|
87
|
+
self.time_scale = time_scale
|
|
88
|
+
self.signals: list[WaveDromSignal] = []
|
|
89
|
+
self.edges: list[WaveDromEdge] = []
|
|
90
|
+
self.config: dict[str, Any] = {"hscale": 2, "skin": "narrow"}
|
|
91
|
+
self._time_offset = 0.0
|
|
92
|
+
self._time_end = 0.0
|
|
93
|
+
|
|
94
|
+
def add_clock(
|
|
95
|
+
self,
|
|
96
|
+
name: str,
|
|
97
|
+
*,
|
|
98
|
+
period: float,
|
|
99
|
+
start_time: float = 0.0,
|
|
100
|
+
duty_cycle: float = 0.5,
|
|
101
|
+
initial_state: Literal["high", "low"] = "low",
|
|
102
|
+
duration: float | None = None,
|
|
103
|
+
) -> None:
|
|
104
|
+
"""Add a clock signal.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
name: Signal name.
|
|
108
|
+
period: Clock period in seconds.
|
|
109
|
+
start_time: Start time in seconds.
|
|
110
|
+
duty_cycle: Duty cycle (0.0-1.0).
|
|
111
|
+
initial_state: Initial clock state.
|
|
112
|
+
duration: Optional duration (defaults to auto-calculated).
|
|
113
|
+
"""
|
|
114
|
+
if duration is None:
|
|
115
|
+
duration = max(self._time_end - start_time, period * 10)
|
|
116
|
+
|
|
117
|
+
# Convert to time steps
|
|
118
|
+
time_steps = int((start_time - self._time_offset) / self.time_scale)
|
|
119
|
+
num_periods = int(duration / period)
|
|
120
|
+
|
|
121
|
+
# Build wave string
|
|
122
|
+
wave = "." * time_steps # Leading dots
|
|
123
|
+
|
|
124
|
+
if initial_state == "low":
|
|
125
|
+
wave += "p" * num_periods # pulsing clock
|
|
126
|
+
else:
|
|
127
|
+
wave += "n" * num_periods # inverted pulsing clock
|
|
128
|
+
|
|
129
|
+
self.signals.append(WaveDromSignal(name=name, wave=wave))
|
|
130
|
+
self._time_end = max(self._time_end, start_time + duration)
|
|
131
|
+
|
|
132
|
+
def add_signal(
|
|
133
|
+
self,
|
|
134
|
+
name: str,
|
|
135
|
+
*,
|
|
136
|
+
edges: list[float] | None = None,
|
|
137
|
+
wave_string: str | None = None,
|
|
138
|
+
data: list[str] | None = None,
|
|
139
|
+
nodes: str | None = None,
|
|
140
|
+
) -> None:
|
|
141
|
+
"""Add a digital signal.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
name: Signal name.
|
|
145
|
+
edges: List of edge timestamps (rising/falling alternating).
|
|
146
|
+
wave_string: Direct WaveDrom wave string (overrides edges).
|
|
147
|
+
data: Optional data labels.
|
|
148
|
+
nodes: Optional node markers (e.g., "A.B.C").
|
|
149
|
+
"""
|
|
150
|
+
if wave_string is not None:
|
|
151
|
+
# Use direct wave string
|
|
152
|
+
self.signals.append(WaveDromSignal(name=name, wave=wave_string, data=data, node=nodes))
|
|
153
|
+
return
|
|
154
|
+
|
|
155
|
+
if edges is None:
|
|
156
|
+
raise ValueError("Must provide either edges or wave_string")
|
|
157
|
+
|
|
158
|
+
# Convert edges to wave string
|
|
159
|
+
wave = self._edges_to_wave(edges)
|
|
160
|
+
self.signals.append(WaveDromSignal(name=name, wave=wave, data=data, node=nodes))
|
|
161
|
+
|
|
162
|
+
def add_data_bus(
|
|
163
|
+
self,
|
|
164
|
+
name: str,
|
|
165
|
+
*,
|
|
166
|
+
transitions: list[tuple[float, str]],
|
|
167
|
+
initial_value: str = "x",
|
|
168
|
+
) -> None:
|
|
169
|
+
"""Add a data bus with labeled values.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
name: Bus name.
|
|
173
|
+
transitions: List of (timestamp, value) tuples.
|
|
174
|
+
initial_value: Initial bus value.
|
|
175
|
+
"""
|
|
176
|
+
if not transitions:
|
|
177
|
+
raise ValueError("Must provide at least one transition")
|
|
178
|
+
|
|
179
|
+
# Sort by timestamp
|
|
180
|
+
transitions_sorted = sorted(transitions, key=lambda x: x[0])
|
|
181
|
+
|
|
182
|
+
# Build wave string and data labels
|
|
183
|
+
wave = ""
|
|
184
|
+
data: list[str] = []
|
|
185
|
+
current_time = self._time_offset
|
|
186
|
+
|
|
187
|
+
for timestamp, value in transitions_sorted:
|
|
188
|
+
# Add stable periods
|
|
189
|
+
steps = int((timestamp - current_time) / self.time_scale)
|
|
190
|
+
if steps > 0:
|
|
191
|
+
wave += "." * (steps - 1)
|
|
192
|
+
|
|
193
|
+
# Add transition
|
|
194
|
+
wave += "="
|
|
195
|
+
data.append(value)
|
|
196
|
+
current_time = timestamp
|
|
197
|
+
|
|
198
|
+
self.signals.append(WaveDromSignal(name=name, wave=wave, data=data))
|
|
199
|
+
|
|
200
|
+
def add_arrow(
|
|
201
|
+
self,
|
|
202
|
+
from_time: float,
|
|
203
|
+
to_time: float,
|
|
204
|
+
label: str,
|
|
205
|
+
*,
|
|
206
|
+
from_signal_idx: int = 0,
|
|
207
|
+
to_signal_idx: int = 0,
|
|
208
|
+
) -> None:
|
|
209
|
+
"""Add an arrow annotation between two time points.
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
from_time: Start time in seconds.
|
|
213
|
+
to_time: End time in seconds.
|
|
214
|
+
label: Arrow label text.
|
|
215
|
+
from_signal_idx: Source signal index.
|
|
216
|
+
to_signal_idx: Destination signal index.
|
|
217
|
+
"""
|
|
218
|
+
# Create node markers
|
|
219
|
+
from_node = f"A{len(self.edges)}"
|
|
220
|
+
to_node = f"B{len(self.edges)}"
|
|
221
|
+
|
|
222
|
+
# Add nodes to signals (simplified - would need to track positions)
|
|
223
|
+
self.edges.append(WaveDromEdge(from_node=from_node, to_node=to_node, label=label))
|
|
224
|
+
|
|
225
|
+
def add_group(self, name: str, signals: list[WaveDromSignal]) -> None:
|
|
226
|
+
"""Add a group of signals.
|
|
227
|
+
|
|
228
|
+
Args:
|
|
229
|
+
name: Group name.
|
|
230
|
+
signals: List of signals in group.
|
|
231
|
+
"""
|
|
232
|
+
# WaveDrom groups are represented as nested lists
|
|
233
|
+
# This is a simplified implementation
|
|
234
|
+
|
|
235
|
+
def _edges_to_wave(self, edges: list[float]) -> str:
|
|
236
|
+
"""Convert edge timestamps to WaveDrom wave string.
|
|
237
|
+
|
|
238
|
+
Args:
|
|
239
|
+
edges: List of edge timestamps.
|
|
240
|
+
|
|
241
|
+
Returns:
|
|
242
|
+
WaveDrom wave string.
|
|
243
|
+
"""
|
|
244
|
+
if not edges:
|
|
245
|
+
return "0"
|
|
246
|
+
|
|
247
|
+
wave = ""
|
|
248
|
+
current_time = self._time_offset
|
|
249
|
+
state = 0 # Start low
|
|
250
|
+
|
|
251
|
+
for edge_time in sorted(edges):
|
|
252
|
+
# Calculate steps to this edge
|
|
253
|
+
steps = int((edge_time - current_time) / self.time_scale)
|
|
254
|
+
|
|
255
|
+
# Add stable period
|
|
256
|
+
if steps > 0:
|
|
257
|
+
wave += "." * (steps - 1)
|
|
258
|
+
|
|
259
|
+
# Add transition
|
|
260
|
+
if state == 0:
|
|
261
|
+
wave += "1" # Rising edge
|
|
262
|
+
state = 1
|
|
263
|
+
else:
|
|
264
|
+
wave += "0" # Falling edge
|
|
265
|
+
state = 0
|
|
266
|
+
|
|
267
|
+
current_time = edge_time
|
|
268
|
+
|
|
269
|
+
return wave if wave else "0"
|
|
270
|
+
|
|
271
|
+
def to_dict(self) -> dict[str, Any]:
|
|
272
|
+
"""Export to WaveDrom dictionary.
|
|
273
|
+
|
|
274
|
+
Returns:
|
|
275
|
+
Dictionary in WaveDrom JSON format.
|
|
276
|
+
"""
|
|
277
|
+
result: dict[str, Any] = {}
|
|
278
|
+
|
|
279
|
+
if self.title:
|
|
280
|
+
result["head"] = {"text": self.title}
|
|
281
|
+
|
|
282
|
+
# Build signal list
|
|
283
|
+
signal_list: list[dict[str, Any]] = []
|
|
284
|
+
for sig in self.signals:
|
|
285
|
+
sig_dict: dict[str, Any] = {"name": sig.name, "wave": sig.wave}
|
|
286
|
+
if sig.data:
|
|
287
|
+
sig_dict["data"] = sig.data
|
|
288
|
+
if sig.node:
|
|
289
|
+
sig_dict["node"] = sig.node
|
|
290
|
+
signal_list.append(sig_dict)
|
|
291
|
+
|
|
292
|
+
result["signal"] = signal_list
|
|
293
|
+
|
|
294
|
+
# Add edges if any
|
|
295
|
+
if self.edges:
|
|
296
|
+
edge_list = [f"{e.from_node}{e.style}>{e.to_node} {e.label}" for e in self.edges]
|
|
297
|
+
result["edge"] = edge_list
|
|
298
|
+
|
|
299
|
+
# Add configuration
|
|
300
|
+
if self.config:
|
|
301
|
+
result["config"] = self.config
|
|
302
|
+
|
|
303
|
+
return result
|
|
304
|
+
|
|
305
|
+
def to_json(self, *, indent: int = 2) -> str:
|
|
306
|
+
"""Export to WaveDrom JSON string.
|
|
307
|
+
|
|
308
|
+
Args:
|
|
309
|
+
indent: JSON indentation level.
|
|
310
|
+
|
|
311
|
+
Returns:
|
|
312
|
+
JSON string.
|
|
313
|
+
"""
|
|
314
|
+
return json.dumps(self.to_dict(), indent=indent)
|
|
315
|
+
|
|
316
|
+
def save(self, filepath: str | Path) -> None:
|
|
317
|
+
"""Save to file.
|
|
318
|
+
|
|
319
|
+
Args:
|
|
320
|
+
filepath: Output file path.
|
|
321
|
+
"""
|
|
322
|
+
filepath = Path(filepath)
|
|
323
|
+
with filepath.open("w") as f:
|
|
324
|
+
f.write(self.to_json())
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def from_digital_trace(
|
|
328
|
+
trace: DigitalTrace,
|
|
329
|
+
*,
|
|
330
|
+
name: str = "signal",
|
|
331
|
+
start_time: float = 0.0,
|
|
332
|
+
duration: float | None = None,
|
|
333
|
+
) -> WaveDromSignal:
|
|
334
|
+
"""Create WaveDrom signal from DigitalTrace.
|
|
335
|
+
|
|
336
|
+
Args:
|
|
337
|
+
trace: Input digital trace.
|
|
338
|
+
name: Signal name.
|
|
339
|
+
start_time: Start time offset.
|
|
340
|
+
duration: Optional duration limit.
|
|
341
|
+
|
|
342
|
+
Returns:
|
|
343
|
+
WaveDromSignal object.
|
|
344
|
+
"""
|
|
345
|
+
# Extract edges from digital trace
|
|
346
|
+
data = trace.data
|
|
347
|
+
transitions = np.diff(data.astype(np.int8))
|
|
348
|
+
|
|
349
|
+
edges: list[float] = []
|
|
350
|
+
time_base = trace.metadata.time_base
|
|
351
|
+
|
|
352
|
+
for i, trans in enumerate(transitions):
|
|
353
|
+
if trans != 0:
|
|
354
|
+
edges.append((i + 1) * time_base + start_time)
|
|
355
|
+
|
|
356
|
+
# Limit duration if specified
|
|
357
|
+
if duration is not None:
|
|
358
|
+
edges = [e for e in edges if e < start_time + duration]
|
|
359
|
+
|
|
360
|
+
# Build wave string
|
|
361
|
+
builder = WaveDromBuilder(time_scale=time_base)
|
|
362
|
+
builder.add_signal(name, edges=edges)
|
|
363
|
+
|
|
364
|
+
return builder.signals[0]
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def export_wavedrom(
|
|
368
|
+
signals: dict[str, WaveformTrace | DigitalTrace],
|
|
369
|
+
filepath: str | Path,
|
|
370
|
+
*,
|
|
371
|
+
title: str | None = None,
|
|
372
|
+
time_scale: float = 1e-9,
|
|
373
|
+
annotations: list[tuple[float, float, str]] | None = None,
|
|
374
|
+
) -> None:
|
|
375
|
+
"""Export signals to WaveDrom JSON file.
|
|
376
|
+
|
|
377
|
+
Args:
|
|
378
|
+
signals: Dictionary mapping signal names to traces.
|
|
379
|
+
filepath: Output file path.
|
|
380
|
+
title: Optional diagram title.
|
|
381
|
+
time_scale: Time scale for conversion (default 1ns).
|
|
382
|
+
annotations: Optional list of (from_time, to_time, label) tuples.
|
|
383
|
+
|
|
384
|
+
Example:
|
|
385
|
+
>>> signals = {
|
|
386
|
+
... "CLK": clock_trace,
|
|
387
|
+
... "DATA": data_trace,
|
|
388
|
+
... "CS": cs_trace,
|
|
389
|
+
... }
|
|
390
|
+
>>> export_wavedrom(signals, "timing.json", title="SPI Transaction")
|
|
391
|
+
"""
|
|
392
|
+
builder = WaveDromBuilder(title=title, time_scale=time_scale)
|
|
393
|
+
|
|
394
|
+
# Add signals
|
|
395
|
+
for name, trace in signals.items():
|
|
396
|
+
# Detect if clock-like
|
|
397
|
+
from oscura.analyzers.waveform.measurements import frequency
|
|
398
|
+
from oscura.core.types import WaveformTrace
|
|
399
|
+
|
|
400
|
+
# frequency() only works with WaveformTrace, skip for DigitalTrace
|
|
401
|
+
if isinstance(trace, WaveformTrace):
|
|
402
|
+
freq = frequency(trace)
|
|
403
|
+
else:
|
|
404
|
+
freq = np.nan
|
|
405
|
+
if not np.isnan(freq) and freq > 0:
|
|
406
|
+
# Looks like a clock
|
|
407
|
+
period = float(1.0 / freq)
|
|
408
|
+
builder.add_clock(name, period=period)
|
|
409
|
+
else:
|
|
410
|
+
# Regular signal - extract edges
|
|
411
|
+
from oscura.analyzers.digital import detect_edges
|
|
412
|
+
|
|
413
|
+
edges = detect_edges(trace)
|
|
414
|
+
builder.add_signal(name, edges=list(edges))
|
|
415
|
+
|
|
416
|
+
# Add annotations
|
|
417
|
+
if annotations:
|
|
418
|
+
for from_t, to_t, label in annotations:
|
|
419
|
+
builder.add_arrow(from_t, to_t, label)
|
|
420
|
+
|
|
421
|
+
builder.save(filepath)
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
__all__ = [
|
|
425
|
+
"WaveDromBuilder",
|
|
426
|
+
"WaveDromEdge",
|
|
427
|
+
"WaveDromSignal",
|
|
428
|
+
"export_wavedrom",
|
|
429
|
+
"from_digital_trace",
|
|
430
|
+
]
|
oscura/exporters/json_export.py
CHANGED
|
@@ -263,6 +263,52 @@ def export_protocol_decode(
|
|
|
263
263
|
json.dump(output, f, cls=OscuraJSONEncoder)
|
|
264
264
|
|
|
265
265
|
|
|
266
|
+
def export_vintage_logic_json(
|
|
267
|
+
result: Any,
|
|
268
|
+
path: str | Path,
|
|
269
|
+
*,
|
|
270
|
+
indent: int = 2,
|
|
271
|
+
include_metadata: bool = True,
|
|
272
|
+
) -> None:
|
|
273
|
+
"""Export vintage logic analysis results to JSON.
|
|
274
|
+
|
|
275
|
+
Convenience function for exporting VintageLogicAnalysisResult objects.
|
|
276
|
+
The dataclass is automatically serialized to JSON with all nested objects.
|
|
277
|
+
|
|
278
|
+
Args:
|
|
279
|
+
result: VintageLogicAnalysisResult object.
|
|
280
|
+
path: Output JSON file path.
|
|
281
|
+
indent: Indentation level for pretty printing (default: 2).
|
|
282
|
+
include_metadata: Include export metadata (default: True).
|
|
283
|
+
|
|
284
|
+
Example:
|
|
285
|
+
>>> from oscura.analyzers.digital.vintage import analyze_vintage_logic
|
|
286
|
+
>>> result = analyze_vintage_logic(traces)
|
|
287
|
+
>>> export_vintage_logic_json(result, "analysis_results.json")
|
|
288
|
+
"""
|
|
289
|
+
path = Path(path)
|
|
290
|
+
|
|
291
|
+
output: dict[str, Any] = {}
|
|
292
|
+
|
|
293
|
+
if include_metadata:
|
|
294
|
+
output["_metadata"] = {
|
|
295
|
+
"format": "oscura_vintage_logic",
|
|
296
|
+
"version": "1.0",
|
|
297
|
+
"exported_at": datetime.now().isoformat(),
|
|
298
|
+
"analysis_timestamp": result.timestamp.isoformat(),
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
output["analysis_result"] = result
|
|
302
|
+
|
|
303
|
+
# Sanitize to handle inf/nan
|
|
304
|
+
from oscura.reporting.output import _sanitize_for_serialization
|
|
305
|
+
|
|
306
|
+
output = _sanitize_for_serialization(output)
|
|
307
|
+
|
|
308
|
+
with open(path, "w") as f:
|
|
309
|
+
json.dump(output, f, cls=OscuraJSONEncoder, indent=indent)
|
|
310
|
+
|
|
311
|
+
|
|
266
312
|
def load_json(path: str | Path) -> dict[str, Any]:
|
|
267
313
|
"""Load JSON data file.
|
|
268
314
|
|
|
@@ -287,5 +333,6 @@ __all__ = [
|
|
|
287
333
|
"export_json",
|
|
288
334
|
"export_measurements",
|
|
289
335
|
"export_protocol_decode",
|
|
336
|
+
"export_vintage_logic_json",
|
|
290
337
|
"load_json",
|
|
291
338
|
]
|