oscura 0.6.0__py3-none-any.whl → 0.8.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 -1
- oscura/analyzers/eye/__init__.py +5 -1
- oscura/analyzers/eye/generation.py +501 -0
- oscura/analyzers/jitter/__init__.py +6 -6
- oscura/analyzers/jitter/timing.py +419 -0
- oscura/analyzers/patterns/__init__.py +28 -0
- oscura/analyzers/patterns/reverse_engineering.py +991 -0
- oscura/analyzers/power/__init__.py +35 -12
- oscura/analyzers/statistics/__init__.py +4 -0
- oscura/analyzers/statistics/basic.py +149 -0
- oscura/analyzers/statistics/correlation.py +47 -6
- oscura/analyzers/waveform/__init__.py +2 -0
- oscura/analyzers/waveform/measurements.py +145 -23
- oscura/analyzers/waveform/spectral.py +361 -8
- oscura/automotive/__init__.py +1 -1
- oscura/core/config/loader.py +0 -1
- oscura/core/types.py +108 -0
- oscura/loaders/__init__.py +12 -4
- oscura/loaders/tss.py +456 -0
- oscura/reporting/__init__.py +88 -1
- oscura/reporting/automation.py +348 -0
- oscura/reporting/citations.py +374 -0
- oscura/reporting/core.py +54 -0
- oscura/reporting/formatting/__init__.py +11 -0
- oscura/reporting/formatting/measurements.py +279 -0
- oscura/reporting/html.py +57 -0
- oscura/reporting/interpretation.py +431 -0
- oscura/reporting/summary.py +329 -0
- oscura/reporting/visualization.py +542 -0
- oscura/visualization/__init__.py +2 -1
- oscura/visualization/batch.py +521 -0
- oscura/workflows/__init__.py +2 -0
- oscura/workflows/waveform.py +783 -0
- {oscura-0.6.0.dist-info → oscura-0.8.0.dist-info}/METADATA +37 -19
- {oscura-0.6.0.dist-info → oscura-0.8.0.dist-info}/RECORD +38 -26
- {oscura-0.6.0.dist-info → oscura-0.8.0.dist-info}/WHEEL +0 -0
- {oscura-0.6.0.dist-info → oscura-0.8.0.dist-info}/entry_points.txt +0 -0
- {oscura-0.6.0.dist-info → oscura-0.8.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,783 @@
|
|
|
1
|
+
"""Complete waveform analysis workflow with full reverse engineering.
|
|
2
|
+
|
|
3
|
+
This module provides high-level APIs for comprehensive waveform analysis,
|
|
4
|
+
automating the entire pipeline from loading to protocol reverse engineering.
|
|
5
|
+
|
|
6
|
+
Example:
|
|
7
|
+
>>> from oscura.workflows import waveform
|
|
8
|
+
>>> # Complete analysis including reverse engineering
|
|
9
|
+
>>> results = waveform.analyze_complete(
|
|
10
|
+
... "unknown_signal.wfm",
|
|
11
|
+
... output_dir="./analysis_output",
|
|
12
|
+
... enable_protocol_decode=True,
|
|
13
|
+
... enable_reverse_engineering=True,
|
|
14
|
+
... generate_plots=True,
|
|
15
|
+
... generate_report=True
|
|
16
|
+
... )
|
|
17
|
+
>>> print(f"Detected protocols: {results['protocols_detected']}")
|
|
18
|
+
>>> print(f"Report saved: {results['report_path']}")
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import Any, Literal
|
|
25
|
+
|
|
26
|
+
import numpy as np
|
|
27
|
+
|
|
28
|
+
import oscura as osc
|
|
29
|
+
from oscura.core.types import DigitalTrace, WaveformTrace
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def analyze_complete(
|
|
33
|
+
filepath: str | Path,
|
|
34
|
+
*,
|
|
35
|
+
output_dir: str | Path | None = None,
|
|
36
|
+
analyses: list[str] | Literal["all"] = "all",
|
|
37
|
+
generate_plots: bool = True,
|
|
38
|
+
generate_report: bool = True,
|
|
39
|
+
embed_plots: bool = True,
|
|
40
|
+
report_format: str = "html",
|
|
41
|
+
# Phase 3: Advanced capabilities
|
|
42
|
+
enable_protocol_decode: bool = True,
|
|
43
|
+
enable_reverse_engineering: bool = True,
|
|
44
|
+
enable_pattern_recognition: bool = True,
|
|
45
|
+
protocol_hints: list[str] | None = None,
|
|
46
|
+
reverse_engineering_depth: Literal["quick", "standard", "deep"] = "standard",
|
|
47
|
+
verbose: bool = True,
|
|
48
|
+
) -> dict[str, Any]:
|
|
49
|
+
"""Perform complete waveform analysis workflow with reverse engineering.
|
|
50
|
+
|
|
51
|
+
This orchestrates the entire analysis pipeline:
|
|
52
|
+
1. Load waveform file (auto-detects format)
|
|
53
|
+
2. Detect signal type (analog/digital)
|
|
54
|
+
3. Run basic analyses (time/frequency/digital/statistical domains)
|
|
55
|
+
4. Protocol detection and decoding (for digital signals)
|
|
56
|
+
5. Reverse engineering pipeline (clock recovery, framing, CRC analysis)
|
|
57
|
+
6. Pattern recognition and anomaly detection
|
|
58
|
+
7. Generate comprehensive visualizations
|
|
59
|
+
8. Create professional report with all findings
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
filepath: Path to waveform file (.wfm, .tss, .csv, etc.).
|
|
63
|
+
output_dir: Output directory for plots and reports.
|
|
64
|
+
Defaults to "./waveform_analysis_output".
|
|
65
|
+
analyses: List of analysis types to run or "all".
|
|
66
|
+
Options: "time_domain", "frequency_domain", "digital", "statistics".
|
|
67
|
+
generate_plots: Whether to generate visualization plots.
|
|
68
|
+
generate_report: Whether to generate HTML/PDF report.
|
|
69
|
+
embed_plots: Whether to embed plots in report (vs external files).
|
|
70
|
+
report_format: Report format ("html" or "pdf").
|
|
71
|
+
enable_protocol_decode: Enable automatic protocol detection and decoding.
|
|
72
|
+
enable_reverse_engineering: Enable reverse engineering pipeline.
|
|
73
|
+
enable_pattern_recognition: Enable pattern mining and anomaly detection.
|
|
74
|
+
protocol_hints: Optional protocol hints for decoder (e.g., ["uart", "spi"]).
|
|
75
|
+
reverse_engineering_depth: RE analysis depth ("quick", "standard", "deep").
|
|
76
|
+
verbose: Print progress messages.
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
Dictionary containing:
|
|
80
|
+
- "filepath": Input file path
|
|
81
|
+
- "trace": Loaded trace object
|
|
82
|
+
- "is_digital": Boolean indicating digital signal
|
|
83
|
+
- "results": Dict of analysis results by domain
|
|
84
|
+
- "protocols_detected": List of detected protocols (if enabled)
|
|
85
|
+
- "decoded_frames": List of decoded protocol frames (if enabled)
|
|
86
|
+
- "reverse_engineering": RE analysis results (if enabled)
|
|
87
|
+
- "patterns": Pattern recognition results (if enabled)
|
|
88
|
+
- "anomalies": Detected anomalies (if enabled)
|
|
89
|
+
- "plots": Dict of plot data (if generate_plots=True)
|
|
90
|
+
- "report_path": Path to generated report (if generate_report=True)
|
|
91
|
+
- "output_dir": Output directory path
|
|
92
|
+
|
|
93
|
+
Raises:
|
|
94
|
+
FileNotFoundError: If filepath does not exist.
|
|
95
|
+
ValueError: If analyses contains invalid analysis type.
|
|
96
|
+
|
|
97
|
+
Example:
|
|
98
|
+
>>> # Minimal usage - full analysis with defaults
|
|
99
|
+
>>> results = analyze_complete("signal.wfm")
|
|
100
|
+
|
|
101
|
+
>>> # Custom configuration
|
|
102
|
+
>>> results = analyze_complete(
|
|
103
|
+
... "complex_signal.tss",
|
|
104
|
+
... output_dir="./my_analysis",
|
|
105
|
+
... analyses=["time_domain", "frequency_domain"],
|
|
106
|
+
... enable_protocol_decode=True,
|
|
107
|
+
... protocol_hints=["uart", "spi"],
|
|
108
|
+
... reverse_engineering_depth="deep",
|
|
109
|
+
... generate_plots=True,
|
|
110
|
+
... generate_report=True
|
|
111
|
+
... )
|
|
112
|
+
|
|
113
|
+
>>> # Access results
|
|
114
|
+
>>> if results["protocols_detected"]:
|
|
115
|
+
... for proto in results["protocols_detected"]:
|
|
116
|
+
... print(f"Found {proto['protocol']} at {proto['baud_rate']} baud")
|
|
117
|
+
>>> if results["reverse_engineering"]:
|
|
118
|
+
... print(f"Baud: {results['reverse_engineering']['baud_rate']}")
|
|
119
|
+
"""
|
|
120
|
+
filepath = Path(filepath)
|
|
121
|
+
if not filepath.exists():
|
|
122
|
+
raise FileNotFoundError(f"File not found: {filepath}")
|
|
123
|
+
|
|
124
|
+
# Set up output directory
|
|
125
|
+
if output_dir is None:
|
|
126
|
+
output_dir = Path("./waveform_analysis_output")
|
|
127
|
+
else:
|
|
128
|
+
output_dir = Path(output_dir)
|
|
129
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
130
|
+
|
|
131
|
+
# Determine which analyses to run
|
|
132
|
+
valid_analyses = {"time_domain", "frequency_domain", "digital", "statistics"}
|
|
133
|
+
if analyses == "all":
|
|
134
|
+
requested_analyses = list(valid_analyses)
|
|
135
|
+
else:
|
|
136
|
+
requested_analyses = analyses
|
|
137
|
+
invalid = set(requested_analyses) - valid_analyses
|
|
138
|
+
if invalid:
|
|
139
|
+
raise ValueError(f"Invalid analysis types: {invalid}. Valid: {valid_analyses}")
|
|
140
|
+
|
|
141
|
+
if verbose:
|
|
142
|
+
print("=" * 80)
|
|
143
|
+
print("OSCURA COMPLETE WAVEFORM ANALYSIS WITH REVERSE ENGINEERING")
|
|
144
|
+
print("=" * 80)
|
|
145
|
+
print(f"\nLoading: {filepath.name}")
|
|
146
|
+
|
|
147
|
+
# Step 1: Load waveform
|
|
148
|
+
trace = osc.load(filepath)
|
|
149
|
+
|
|
150
|
+
# Detect signal type using new properties
|
|
151
|
+
is_digital = (
|
|
152
|
+
trace.is_digital if hasattr(trace, "is_digital") else isinstance(trace, DigitalTrace)
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
if verbose:
|
|
156
|
+
signal_type = "Digital" if is_digital else "Analog"
|
|
157
|
+
print(f"✓ Loaded {signal_type} signal")
|
|
158
|
+
print(f" Samples: {len(trace)}")
|
|
159
|
+
print(f" Sample rate: {trace.metadata.sample_rate:.2e} Hz")
|
|
160
|
+
print(f" Duration: {trace.duration:.6f} s")
|
|
161
|
+
|
|
162
|
+
# Step 2: Run basic analyses
|
|
163
|
+
results: dict[str, dict[str, Any]] = {}
|
|
164
|
+
|
|
165
|
+
if "time_domain" in requested_analyses:
|
|
166
|
+
if verbose:
|
|
167
|
+
print("\n" + "=" * 80)
|
|
168
|
+
print("TIME-DOMAIN ANALYSIS")
|
|
169
|
+
print("=" * 80)
|
|
170
|
+
|
|
171
|
+
# Run time-domain measurements - GET ALL AVAILABLE
|
|
172
|
+
if isinstance(trace, WaveformTrace):
|
|
173
|
+
from oscura.analyzers import waveform as waveform_analyzer
|
|
174
|
+
|
|
175
|
+
# Pass parameters=None to get ALL available measurements
|
|
176
|
+
time_results = waveform_analyzer.measure(trace, parameters=None, include_units=True)
|
|
177
|
+
results["time_domain"] = time_results
|
|
178
|
+
if verbose:
|
|
179
|
+
print(f"✓ Completed {len(time_results)} measurements")
|
|
180
|
+
|
|
181
|
+
if "frequency_domain" in requested_analyses and not is_digital:
|
|
182
|
+
if verbose:
|
|
183
|
+
print("\n" + "=" * 80)
|
|
184
|
+
print("FREQUENCY-DOMAIN ANALYSIS")
|
|
185
|
+
print("=" * 80)
|
|
186
|
+
|
|
187
|
+
if isinstance(trace, WaveformTrace):
|
|
188
|
+
# Run spectral analysis using unified measure() API
|
|
189
|
+
from oscura.analyzers.waveform import spectral
|
|
190
|
+
|
|
191
|
+
freq_results = spectral.measure(trace, include_units=True)
|
|
192
|
+
|
|
193
|
+
# Add FFT arrays for plotting (not measurements)
|
|
194
|
+
try:
|
|
195
|
+
fft_result = osc.fft(trace)
|
|
196
|
+
freq_results["fft_freqs"] = fft_result[0]
|
|
197
|
+
freq_results["fft_data"] = fft_result[1]
|
|
198
|
+
except Exception:
|
|
199
|
+
pass
|
|
200
|
+
|
|
201
|
+
results["frequency_domain"] = freq_results
|
|
202
|
+
if verbose:
|
|
203
|
+
# Count actual measurements (not arrays)
|
|
204
|
+
numeric_count = sum(
|
|
205
|
+
1
|
|
206
|
+
for k, v in freq_results.items()
|
|
207
|
+
if k not in ["fft_freqs", "fft_data"] and isinstance(v, (int, float, dict))
|
|
208
|
+
)
|
|
209
|
+
print(f"✓ Completed {numeric_count} measurements")
|
|
210
|
+
|
|
211
|
+
if "digital" in requested_analyses:
|
|
212
|
+
if verbose:
|
|
213
|
+
print("\n" + "=" * 80)
|
|
214
|
+
print("DIGITAL SIGNAL ANALYSIS")
|
|
215
|
+
print("=" * 80)
|
|
216
|
+
|
|
217
|
+
# Run comprehensive digital analysis (works for both analog and digital traces)
|
|
218
|
+
try:
|
|
219
|
+
from oscura.analyzers.digital import signal_quality_summary, timing
|
|
220
|
+
|
|
221
|
+
# Convert DigitalTrace to WaveformTrace for analysis
|
|
222
|
+
analysis_trace = trace
|
|
223
|
+
if isinstance(trace, DigitalTrace):
|
|
224
|
+
# Convert bool array to float for analysis
|
|
225
|
+
waveform_data = trace.data.astype(float)
|
|
226
|
+
analysis_trace = WaveformTrace(data=waveform_data, metadata=trace.metadata)
|
|
227
|
+
|
|
228
|
+
if isinstance(analysis_trace, WaveformTrace):
|
|
229
|
+
# Get signal quality summary
|
|
230
|
+
digital_results_obj = signal_quality_summary(analysis_trace)
|
|
231
|
+
digital_results: dict[str, Any]
|
|
232
|
+
if hasattr(digital_results_obj, "__dict__"):
|
|
233
|
+
digital_results = digital_results_obj.__dict__
|
|
234
|
+
else:
|
|
235
|
+
digital_results = dict(digital_results_obj)
|
|
236
|
+
|
|
237
|
+
# Add timing measurements
|
|
238
|
+
try:
|
|
239
|
+
# Slew rate for rising and falling edges
|
|
240
|
+
slew_rising = timing.slew_rate(
|
|
241
|
+
analysis_trace, edge_type="rising", return_all=False
|
|
242
|
+
)
|
|
243
|
+
if not np.isnan(slew_rising):
|
|
244
|
+
digital_results["slew_rate_rising"] = slew_rising
|
|
245
|
+
|
|
246
|
+
slew_falling = timing.slew_rate(
|
|
247
|
+
analysis_trace, edge_type="falling", return_all=False
|
|
248
|
+
)
|
|
249
|
+
if not np.isnan(slew_falling):
|
|
250
|
+
digital_results["slew_rate_falling"] = slew_falling
|
|
251
|
+
except Exception:
|
|
252
|
+
pass # Skip if slew rate not applicable
|
|
253
|
+
|
|
254
|
+
results["digital"] = digital_results
|
|
255
|
+
if verbose:
|
|
256
|
+
numeric_count = sum(
|
|
257
|
+
1 for v in digital_results.values() if isinstance(v, (int, float))
|
|
258
|
+
)
|
|
259
|
+
print(f"✓ Completed {numeric_count} measurements")
|
|
260
|
+
except Exception as e:
|
|
261
|
+
if verbose:
|
|
262
|
+
print(f" ⚠ Digital analysis unavailable: {e}")
|
|
263
|
+
|
|
264
|
+
if "statistics" in requested_analyses and not is_digital:
|
|
265
|
+
if verbose:
|
|
266
|
+
print("\n" + "=" * 80)
|
|
267
|
+
print("STATISTICAL ANALYSIS")
|
|
268
|
+
print("=" * 80)
|
|
269
|
+
|
|
270
|
+
if isinstance(trace, WaveformTrace):
|
|
271
|
+
# Run statistical analysis using unified measure() API
|
|
272
|
+
from oscura.analyzers import statistics
|
|
273
|
+
|
|
274
|
+
stats_results = statistics.measure(trace.data, include_units=True)
|
|
275
|
+
results["statistics"] = stats_results
|
|
276
|
+
if verbose:
|
|
277
|
+
numeric_count = len(stats_results)
|
|
278
|
+
print(f"✓ Completed {numeric_count} measurements")
|
|
279
|
+
|
|
280
|
+
# Step 3: Protocol Detection & Decoding (FULL IMPLEMENTATION)
|
|
281
|
+
protocols_detected: list[dict[str, Any]] = []
|
|
282
|
+
decoded_frames: list[Any] = []
|
|
283
|
+
|
|
284
|
+
if enable_protocol_decode and is_digital:
|
|
285
|
+
if verbose:
|
|
286
|
+
print("\n" + "=" * 80)
|
|
287
|
+
print("PROTOCOL DETECTION & DECODING")
|
|
288
|
+
print("=" * 80)
|
|
289
|
+
|
|
290
|
+
try:
|
|
291
|
+
from oscura.discovery import auto_decoder
|
|
292
|
+
|
|
293
|
+
# Try each protocol hint or auto-detect
|
|
294
|
+
protocols_to_try = protocol_hints if protocol_hints else ["UART", "SPI", "I2C"]
|
|
295
|
+
|
|
296
|
+
for proto_name in protocols_to_try:
|
|
297
|
+
try:
|
|
298
|
+
# Type narrow to WaveformTrace | DigitalTrace
|
|
299
|
+
if not isinstance(trace, (WaveformTrace, DigitalTrace)):
|
|
300
|
+
continue
|
|
301
|
+
|
|
302
|
+
result = auto_decoder.decode_protocol(
|
|
303
|
+
trace,
|
|
304
|
+
protocol_hint=proto_name.upper(), # type: ignore[arg-type]
|
|
305
|
+
confidence_threshold=0.6, # Lower threshold to catch more
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
if result.overall_confidence >= 0.6:
|
|
309
|
+
proto_info = {
|
|
310
|
+
"protocol": result.protocol,
|
|
311
|
+
"confidence": result.overall_confidence,
|
|
312
|
+
"params": result.detected_params,
|
|
313
|
+
"frame_count": result.frame_count,
|
|
314
|
+
"error_count": result.error_count,
|
|
315
|
+
}
|
|
316
|
+
protocols_detected.append(proto_info)
|
|
317
|
+
decoded_frames.extend(result.data)
|
|
318
|
+
|
|
319
|
+
if verbose:
|
|
320
|
+
print(
|
|
321
|
+
f"✓ Detected {result.protocol.upper()}: "
|
|
322
|
+
f"{result.overall_confidence:.1%} confidence"
|
|
323
|
+
)
|
|
324
|
+
print(
|
|
325
|
+
f" Decoded {len(result.data)} bytes, {result.frame_count} frames"
|
|
326
|
+
)
|
|
327
|
+
except Exception:
|
|
328
|
+
# Protocol didn't match, continue trying others
|
|
329
|
+
pass
|
|
330
|
+
|
|
331
|
+
if not protocols_detected and verbose:
|
|
332
|
+
print(" ⚠ No protocols detected (signal may be unknown or noisy)")
|
|
333
|
+
|
|
334
|
+
except Exception as e:
|
|
335
|
+
if verbose:
|
|
336
|
+
print(f" ⚠ Protocol detection unavailable: {e}")
|
|
337
|
+
|
|
338
|
+
# Step 4: Reverse Engineering Pipeline (FULL IMPLEMENTATION)
|
|
339
|
+
reverse_engineering_results: dict[str, Any] | None = None
|
|
340
|
+
|
|
341
|
+
if (
|
|
342
|
+
enable_reverse_engineering
|
|
343
|
+
and is_digital
|
|
344
|
+
and isinstance(trace, (WaveformTrace, DigitalTrace))
|
|
345
|
+
and len(trace.data) > 1000
|
|
346
|
+
):
|
|
347
|
+
if verbose:
|
|
348
|
+
print("\n" + "=" * 80)
|
|
349
|
+
print("REVERSE ENGINEERING ANALYSIS")
|
|
350
|
+
print("=" * 80)
|
|
351
|
+
depth_map = {
|
|
352
|
+
"quick": "Quick (basic)",
|
|
353
|
+
"standard": "Standard (comprehensive)",
|
|
354
|
+
"deep": "Deep (exhaustive)",
|
|
355
|
+
}
|
|
356
|
+
print(f" Mode: {depth_map.get(reverse_engineering_depth, 'Standard')}")
|
|
357
|
+
|
|
358
|
+
try:
|
|
359
|
+
from oscura.workflows import reverse_engineering as re_workflow
|
|
360
|
+
|
|
361
|
+
# Convert DigitalTrace to WaveformTrace for RE
|
|
362
|
+
re_trace = trace
|
|
363
|
+
if isinstance(trace, DigitalTrace):
|
|
364
|
+
waveform_data = trace.data.astype(float)
|
|
365
|
+
re_trace = WaveformTrace(data=waveform_data, metadata=trace.metadata)
|
|
366
|
+
|
|
367
|
+
if isinstance(re_trace, WaveformTrace):
|
|
368
|
+
# Set parameters based on depth
|
|
369
|
+
if reverse_engineering_depth == "quick":
|
|
370
|
+
baud_rates = [9600, 115200]
|
|
371
|
+
min_frames = 2
|
|
372
|
+
elif reverse_engineering_depth == "deep":
|
|
373
|
+
baud_rates = [9600, 19200, 38400, 57600, 115200, 230400, 460800, 921600]
|
|
374
|
+
min_frames = 5
|
|
375
|
+
else: # standard
|
|
376
|
+
baud_rates = [9600, 19200, 38400, 57600, 115200, 230400]
|
|
377
|
+
min_frames = 3
|
|
378
|
+
|
|
379
|
+
re_result = re_workflow.reverse_engineer_signal(
|
|
380
|
+
re_trace,
|
|
381
|
+
expected_baud_rates=baud_rates,
|
|
382
|
+
min_frames=min_frames,
|
|
383
|
+
max_frame_length=256,
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
# Extract key findings
|
|
387
|
+
reverse_engineering_results = {
|
|
388
|
+
"baud_rate": re_result.baud_rate,
|
|
389
|
+
"confidence": re_result.confidence,
|
|
390
|
+
"frame_count": len(re_result.frames),
|
|
391
|
+
"frame_format": re_result.protocol_spec.frame_format,
|
|
392
|
+
"sync_pattern": re_result.protocol_spec.sync_pattern,
|
|
393
|
+
"frame_length": re_result.protocol_spec.frame_length,
|
|
394
|
+
"field_count": len(re_result.protocol_spec.fields),
|
|
395
|
+
"checksum_type": re_result.protocol_spec.checksum_type,
|
|
396
|
+
"checksum_position": re_result.protocol_spec.checksum_position,
|
|
397
|
+
"warnings": re_result.warnings,
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if verbose:
|
|
401
|
+
print(
|
|
402
|
+
f"✓ Baud rate: {re_result.baud_rate:.0f} Hz (confidence: {re_result.confidence:.1%})"
|
|
403
|
+
)
|
|
404
|
+
print(f"✓ Frames: {len(re_result.frames)} detected")
|
|
405
|
+
if re_result.protocol_spec.sync_pattern:
|
|
406
|
+
print(f"✓ Sync pattern: {re_result.protocol_spec.sync_pattern}")
|
|
407
|
+
if re_result.protocol_spec.frame_length:
|
|
408
|
+
print(f"✓ Frame length: {re_result.protocol_spec.frame_length} bytes")
|
|
409
|
+
if re_result.protocol_spec.checksum_type:
|
|
410
|
+
print(f"✓ Checksum: {re_result.protocol_spec.checksum_type}")
|
|
411
|
+
if re_result.warnings:
|
|
412
|
+
print(f" ⚠ Warnings: {len(re_result.warnings)}")
|
|
413
|
+
|
|
414
|
+
except ValueError as e:
|
|
415
|
+
if verbose:
|
|
416
|
+
print(f" ⚠ RE analysis: {e!s}")
|
|
417
|
+
reverse_engineering_results = {"status": "insufficient_data", "message": str(e)}
|
|
418
|
+
except Exception as e:
|
|
419
|
+
if verbose:
|
|
420
|
+
print(f" ⚠ RE analysis unavailable: {e}")
|
|
421
|
+
reverse_engineering_results = {"status": "error", "message": str(e)}
|
|
422
|
+
|
|
423
|
+
# Step 5: Pattern Recognition & Anomaly Detection (FULL IMPLEMENTATION)
|
|
424
|
+
pattern_results: dict[str, Any] | None = None
|
|
425
|
+
anomalies_detected: list[dict[str, Any]] = []
|
|
426
|
+
|
|
427
|
+
if enable_pattern_recognition:
|
|
428
|
+
if verbose:
|
|
429
|
+
print("\n" + "=" * 80)
|
|
430
|
+
print("PATTERN RECOGNITION & ANOMALY DETECTION")
|
|
431
|
+
print("=" * 80)
|
|
432
|
+
|
|
433
|
+
pattern_results = {}
|
|
434
|
+
|
|
435
|
+
# Anomaly detection
|
|
436
|
+
try:
|
|
437
|
+
from oscura.discovery import anomaly_detector
|
|
438
|
+
|
|
439
|
+
# Convert DigitalTrace to WaveformTrace for anomaly detection
|
|
440
|
+
anomaly_trace = trace
|
|
441
|
+
if isinstance(trace, DigitalTrace):
|
|
442
|
+
waveform_data = trace.data.astype(float)
|
|
443
|
+
anomaly_trace = WaveformTrace(data=waveform_data, metadata=trace.metadata)
|
|
444
|
+
|
|
445
|
+
if isinstance(anomaly_trace, WaveformTrace):
|
|
446
|
+
anomalies = anomaly_detector.find_anomalies(
|
|
447
|
+
anomaly_trace,
|
|
448
|
+
min_confidence=0.6,
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
# Convert to list of dicts
|
|
452
|
+
anomalies_detected = [
|
|
453
|
+
{
|
|
454
|
+
"type": a.type,
|
|
455
|
+
"start": float(a.timestamp_us) / 1e6, # Convert to seconds
|
|
456
|
+
"end": float(a.timestamp_us + a.duration_ns / 1000) / 1e6,
|
|
457
|
+
"severity": a.severity,
|
|
458
|
+
"description": a.description,
|
|
459
|
+
}
|
|
460
|
+
for a in anomalies
|
|
461
|
+
]
|
|
462
|
+
pattern_results["anomalies"] = anomalies_detected
|
|
463
|
+
|
|
464
|
+
if verbose and anomalies_detected:
|
|
465
|
+
print(f"✓ Detected {len(anomalies_detected)} anomalies")
|
|
466
|
+
severity_counts: dict[str, int] = {}
|
|
467
|
+
for a in anomalies_detected:
|
|
468
|
+
severity_counts[a["severity"]] = severity_counts.get(a["severity"], 0) + 1
|
|
469
|
+
for sev, count in sorted(severity_counts.items()):
|
|
470
|
+
print(f" - {sev}: {count}")
|
|
471
|
+
except Exception as e:
|
|
472
|
+
if verbose:
|
|
473
|
+
print(f" ⚠ Anomaly detection unavailable: {e}")
|
|
474
|
+
|
|
475
|
+
# Pattern discovery (for byte streams)
|
|
476
|
+
if decoded_frames and len(decoded_frames) > 10:
|
|
477
|
+
try:
|
|
478
|
+
from oscura.analyzers.patterns import discovery
|
|
479
|
+
|
|
480
|
+
# Convert decoded bytes to numpy array
|
|
481
|
+
byte_data = np.array([b.value for b in decoded_frames[:1000]], dtype=np.uint8)
|
|
482
|
+
|
|
483
|
+
signatures = discovery.discover_signatures(byte_data, min_occurrences=3)
|
|
484
|
+
pattern_results["signatures"] = [
|
|
485
|
+
{
|
|
486
|
+
"pattern": sig.pattern.hex(),
|
|
487
|
+
"count": sig.occurrences,
|
|
488
|
+
"confidence": float(sig.score),
|
|
489
|
+
"length": sig.length,
|
|
490
|
+
}
|
|
491
|
+
for sig in signatures[:10] # Top 10
|
|
492
|
+
]
|
|
493
|
+
|
|
494
|
+
if verbose and signatures:
|
|
495
|
+
print(f"✓ Discovered {len(signatures)} signature patterns")
|
|
496
|
+
except Exception as e:
|
|
497
|
+
if verbose:
|
|
498
|
+
print(f" ⚠ Pattern discovery unavailable: {e}")
|
|
499
|
+
|
|
500
|
+
# Step 6: Generate plots (ALL plots from original + RE plots)
|
|
501
|
+
plots: dict[str, str] = {}
|
|
502
|
+
if generate_plots:
|
|
503
|
+
if verbose:
|
|
504
|
+
print("\n" + "=" * 80)
|
|
505
|
+
print("GENERATING VISUALIZATIONS")
|
|
506
|
+
print("=" * 80)
|
|
507
|
+
|
|
508
|
+
from oscura.visualization import batch
|
|
509
|
+
|
|
510
|
+
# Generate ALL standard plots
|
|
511
|
+
if isinstance(trace, (WaveformTrace, DigitalTrace)):
|
|
512
|
+
plots = batch.generate_all_plots(trace, verbose=verbose)
|
|
513
|
+
|
|
514
|
+
if verbose:
|
|
515
|
+
print(f"✓ Generated {len(plots)} total plots")
|
|
516
|
+
|
|
517
|
+
# Step 7: Generate comprehensive report
|
|
518
|
+
report_path: Path | None = None
|
|
519
|
+
if generate_report:
|
|
520
|
+
if verbose:
|
|
521
|
+
print("\n" + "=" * 80)
|
|
522
|
+
print("GENERATING COMPREHENSIVE REPORT")
|
|
523
|
+
print("=" * 80)
|
|
524
|
+
|
|
525
|
+
from oscura.reporting import Report, ReportConfig, generate_html_report
|
|
526
|
+
|
|
527
|
+
# Create report
|
|
528
|
+
valid_format: Literal["html", "pdf", "markdown", "docx"] = (
|
|
529
|
+
"html" if report_format == "html" else "pdf"
|
|
530
|
+
)
|
|
531
|
+
config = ReportConfig(
|
|
532
|
+
title="Complete Waveform Analysis with Reverse Engineering",
|
|
533
|
+
format=valid_format,
|
|
534
|
+
verbosity="detailed",
|
|
535
|
+
)
|
|
536
|
+
|
|
537
|
+
report = Report(
|
|
538
|
+
config=config,
|
|
539
|
+
metadata={
|
|
540
|
+
"file": str(filepath),
|
|
541
|
+
"type": "Digital" if is_digital else "Analog",
|
|
542
|
+
"protocols_detected": len(protocols_detected),
|
|
543
|
+
"frames_decoded": len(decoded_frames),
|
|
544
|
+
"anomalies_found": len(anomalies_detected),
|
|
545
|
+
},
|
|
546
|
+
)
|
|
547
|
+
|
|
548
|
+
# Add basic measurement sections - handle BOTH formats
|
|
549
|
+
for analysis_name, analysis_results in results.items():
|
|
550
|
+
# Extract measurements in both formats:
|
|
551
|
+
# 1. Unified format: {"value": float, "unit": str}
|
|
552
|
+
# 2. Legacy format: flat float/int values
|
|
553
|
+
measurements = {}
|
|
554
|
+
|
|
555
|
+
for k, v in analysis_results.items():
|
|
556
|
+
if isinstance(v, dict) and "value" in v:
|
|
557
|
+
# Unified format - extract value for reporting
|
|
558
|
+
measurements[k] = v["value"]
|
|
559
|
+
elif isinstance(v, (int, float)) and not isinstance(v, bool):
|
|
560
|
+
# Legacy flat format
|
|
561
|
+
measurements[k] = v
|
|
562
|
+
# Skip arrays, objects, etc.
|
|
563
|
+
|
|
564
|
+
if measurements:
|
|
565
|
+
title_map = {
|
|
566
|
+
"time_domain": "Time-Domain Analysis (IEEE 181-2011)",
|
|
567
|
+
"frequency_domain": "Frequency-Domain Analysis (IEEE 1241-2010)",
|
|
568
|
+
"digital": "Digital Signal Analysis",
|
|
569
|
+
"statistics": "Statistical Analysis",
|
|
570
|
+
}
|
|
571
|
+
title = title_map.get(analysis_name, analysis_name.replace("_", " ").title())
|
|
572
|
+
report.add_measurements(title=title, measurements=measurements)
|
|
573
|
+
|
|
574
|
+
# Add protocol detection section
|
|
575
|
+
if protocols_detected:
|
|
576
|
+
report.add_section(
|
|
577
|
+
title="Protocol Detection Results",
|
|
578
|
+
content=_format_protocol_detection(protocols_detected, decoded_frames),
|
|
579
|
+
)
|
|
580
|
+
|
|
581
|
+
# Add reverse engineering section
|
|
582
|
+
if reverse_engineering_results and reverse_engineering_results.get("baud_rate"):
|
|
583
|
+
report.add_section(
|
|
584
|
+
title="Reverse Engineering Analysis",
|
|
585
|
+
content=_format_reverse_engineering(reverse_engineering_results),
|
|
586
|
+
)
|
|
587
|
+
|
|
588
|
+
# Add anomaly detection section
|
|
589
|
+
if anomalies_detected:
|
|
590
|
+
report.add_section(
|
|
591
|
+
title="Anomaly Detection Results",
|
|
592
|
+
content=_format_anomalies(anomalies_detected),
|
|
593
|
+
)
|
|
594
|
+
|
|
595
|
+
# Add pattern recognition section
|
|
596
|
+
if pattern_results and pattern_results.get("signatures"):
|
|
597
|
+
report.add_section(
|
|
598
|
+
title="Pattern Recognition Results",
|
|
599
|
+
content=_format_patterns(pattern_results),
|
|
600
|
+
)
|
|
601
|
+
|
|
602
|
+
# Generate HTML
|
|
603
|
+
html_content = generate_html_report(report)
|
|
604
|
+
|
|
605
|
+
# Embed plots if requested
|
|
606
|
+
if embed_plots and plots:
|
|
607
|
+
from oscura.reporting import embed_plots as embed_plots_func
|
|
608
|
+
|
|
609
|
+
html_content = embed_plots_func(html_content, plots)
|
|
610
|
+
if verbose:
|
|
611
|
+
print(f" ✓ Embedded {len(plots)} plots in report")
|
|
612
|
+
|
|
613
|
+
# Save report
|
|
614
|
+
report_path = output_dir / f"complete_analysis.{report_format}"
|
|
615
|
+
report_path.write_text(html_content, encoding="utf-8")
|
|
616
|
+
|
|
617
|
+
if verbose:
|
|
618
|
+
print(f"✓ Report saved: {report_path}")
|
|
619
|
+
|
|
620
|
+
if verbose:
|
|
621
|
+
print("\n" + "=" * 80)
|
|
622
|
+
print("ANALYSIS COMPLETE")
|
|
623
|
+
print("=" * 80)
|
|
624
|
+
print(f"✓ Output directory: {output_dir}")
|
|
625
|
+
if protocols_detected:
|
|
626
|
+
print(f"✓ Protocols detected: {len(protocols_detected)}")
|
|
627
|
+
if decoded_frames:
|
|
628
|
+
print(f"✓ Frames decoded: {len(decoded_frames)}")
|
|
629
|
+
if anomalies_detected:
|
|
630
|
+
print(f"✓ Anomalies found: {len(anomalies_detected)}")
|
|
631
|
+
|
|
632
|
+
# Return comprehensive results
|
|
633
|
+
return {
|
|
634
|
+
"filepath": filepath,
|
|
635
|
+
"trace": trace,
|
|
636
|
+
"is_digital": is_digital,
|
|
637
|
+
"results": results,
|
|
638
|
+
"protocols_detected": protocols_detected,
|
|
639
|
+
"decoded_frames": decoded_frames,
|
|
640
|
+
"reverse_engineering": reverse_engineering_results,
|
|
641
|
+
"patterns": pattern_results,
|
|
642
|
+
"anomalies": anomalies_detected,
|
|
643
|
+
"plots": plots if generate_plots else {},
|
|
644
|
+
"report_path": report_path,
|
|
645
|
+
"output_dir": output_dir,
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
|
|
649
|
+
def _format_protocol_detection(protocols: list[dict[str, Any]], frames: list[Any]) -> str:
|
|
650
|
+
"""Format protocol detection results for report.
|
|
651
|
+
|
|
652
|
+
Args:
|
|
653
|
+
protocols: List of detected protocols.
|
|
654
|
+
frames: List of decoded frames.
|
|
655
|
+
|
|
656
|
+
Returns:
|
|
657
|
+
HTML formatted string.
|
|
658
|
+
"""
|
|
659
|
+
html = "<h3>Detected Protocols</h3>\n<ul>\n"
|
|
660
|
+
for proto in protocols:
|
|
661
|
+
conf = proto.get("confidence", 0.0)
|
|
662
|
+
html += f"<li><strong>{proto['protocol'].upper()}</strong>: {conf:.1%} confidence"
|
|
663
|
+
if "params" in proto and "baud_rate" in proto["params"]:
|
|
664
|
+
html += f" at {proto['params']['baud_rate']:.0f} baud"
|
|
665
|
+
if proto.get("frame_count"):
|
|
666
|
+
html += f" ({proto['frame_count']} frames)"
|
|
667
|
+
html += "</li>\n"
|
|
668
|
+
html += "</ul>\n"
|
|
669
|
+
|
|
670
|
+
if frames:
|
|
671
|
+
html += f"<p><strong>Total bytes decoded:</strong> {len(frames)}</p>\n"
|
|
672
|
+
|
|
673
|
+
return html
|
|
674
|
+
|
|
675
|
+
|
|
676
|
+
def _format_reverse_engineering(re_results: dict[str, Any]) -> str:
|
|
677
|
+
"""Format reverse engineering results for report.
|
|
678
|
+
|
|
679
|
+
Args:
|
|
680
|
+
re_results: RE analysis results dictionary.
|
|
681
|
+
|
|
682
|
+
Returns:
|
|
683
|
+
HTML formatted string.
|
|
684
|
+
"""
|
|
685
|
+
html = "<h3>Reverse Engineering Findings</h3>\n<ul>\n"
|
|
686
|
+
|
|
687
|
+
if re_results.get("baud_rate"):
|
|
688
|
+
html += f"<li><strong>Baud Rate:</strong> {re_results['baud_rate']:.0f} Hz</li>\n"
|
|
689
|
+
|
|
690
|
+
if re_results.get("confidence"):
|
|
691
|
+
conf = re_results["confidence"]
|
|
692
|
+
html += f"<li><strong>Overall Confidence:</strong> {conf:.1%}</li>\n"
|
|
693
|
+
|
|
694
|
+
if re_results.get("frame_count"):
|
|
695
|
+
html += f"<li><strong>Frames Detected:</strong> {re_results['frame_count']}</li>\n"
|
|
696
|
+
|
|
697
|
+
if re_results.get("frame_format"):
|
|
698
|
+
html += f"<li><strong>Frame Format:</strong> {re_results['frame_format']}</li>\n"
|
|
699
|
+
|
|
700
|
+
if re_results.get("sync_pattern"):
|
|
701
|
+
html += f"<li><strong>Sync Pattern:</strong> {re_results['sync_pattern']}</li>\n"
|
|
702
|
+
|
|
703
|
+
if re_results.get("frame_length"):
|
|
704
|
+
html += f"<li><strong>Frame Length:</strong> {re_results['frame_length']} bytes</li>\n"
|
|
705
|
+
|
|
706
|
+
if re_results.get("field_count"):
|
|
707
|
+
html += f"<li><strong>Fields Identified:</strong> {re_results['field_count']}</li>\n"
|
|
708
|
+
|
|
709
|
+
if re_results.get("checksum_type"):
|
|
710
|
+
html += f"<li><strong>Checksum:</strong> {re_results['checksum_type']}"
|
|
711
|
+
if re_results.get("checksum_position") is not None:
|
|
712
|
+
html += f" at position {re_results['checksum_position']}"
|
|
713
|
+
html += "</li>\n"
|
|
714
|
+
|
|
715
|
+
html += "</ul>\n"
|
|
716
|
+
|
|
717
|
+
if re_results.get("warnings"):
|
|
718
|
+
html += "<h4>Warnings</h4>\n<ul>\n"
|
|
719
|
+
for warning in re_results["warnings"][:5]: # Max 5 warnings
|
|
720
|
+
html += f"<li>{warning}</li>\n"
|
|
721
|
+
html += "</ul>\n"
|
|
722
|
+
|
|
723
|
+
return html
|
|
724
|
+
|
|
725
|
+
|
|
726
|
+
def _format_anomalies(anomalies: list[dict[str, Any]]) -> str:
|
|
727
|
+
"""Format anomaly detection results for report.
|
|
728
|
+
|
|
729
|
+
Args:
|
|
730
|
+
anomalies: List of detected anomalies.
|
|
731
|
+
|
|
732
|
+
Returns:
|
|
733
|
+
HTML formatted string.
|
|
734
|
+
"""
|
|
735
|
+
html = "<h3>Detected Anomalies</h3>\n"
|
|
736
|
+
html += f"<p><strong>Total anomalies:</strong> {len(anomalies)}</p>\n"
|
|
737
|
+
|
|
738
|
+
# Group by severity
|
|
739
|
+
by_severity: dict[str, list[dict[str, Any]]] = {}
|
|
740
|
+
for anomaly in anomalies:
|
|
741
|
+
severity = anomaly.get("severity", "unknown")
|
|
742
|
+
by_severity.setdefault(severity, []).append(anomaly)
|
|
743
|
+
|
|
744
|
+
for severity in ["critical", "warning", "info"]:
|
|
745
|
+
if severity in by_severity:
|
|
746
|
+
html += f"<h4>{severity.title()} ({len(by_severity[severity])})</h4>\n<ul>\n"
|
|
747
|
+
for anomaly in by_severity[severity][:10]: # Max 10 per severity
|
|
748
|
+
html += f"<li><strong>{anomaly['type']}:</strong> {anomaly['description']}"
|
|
749
|
+
html += f" (at {anomaly['start']:.6f}s)</li>\n"
|
|
750
|
+
html += "</ul>\n"
|
|
751
|
+
|
|
752
|
+
return html
|
|
753
|
+
|
|
754
|
+
|
|
755
|
+
def _format_patterns(pattern_results: dict[str, Any]) -> str:
|
|
756
|
+
"""Format pattern recognition results for report.
|
|
757
|
+
|
|
758
|
+
Args:
|
|
759
|
+
pattern_results: Pattern analysis results dictionary.
|
|
760
|
+
|
|
761
|
+
Returns:
|
|
762
|
+
HTML formatted string.
|
|
763
|
+
"""
|
|
764
|
+
html = "<h3>Pattern Recognition Results</h3>\n"
|
|
765
|
+
|
|
766
|
+
if pattern_results.get("signatures"):
|
|
767
|
+
sigs = pattern_results["signatures"]
|
|
768
|
+
html += f"<p><strong>Signature patterns discovered:</strong> {len(sigs)}</p>\n"
|
|
769
|
+
html += "<table border='1' cellpadding='5'>\n"
|
|
770
|
+
html += "<tr><th>Pattern</th><th>Length</th><th>Count</th><th>Score</th></tr>\n"
|
|
771
|
+
for sig in sigs[:10]: # Top 10
|
|
772
|
+
html += f"<tr><td><code>{sig['pattern']}</code></td>"
|
|
773
|
+
html += f"<td>{sig['length']} bytes</td>"
|
|
774
|
+
html += f"<td>{sig['count']}</td>"
|
|
775
|
+
html += f"<td>{sig['confidence']:.2f}</td></tr>\n"
|
|
776
|
+
html += "</table>\n"
|
|
777
|
+
|
|
778
|
+
return html
|
|
779
|
+
|
|
780
|
+
|
|
781
|
+
__all__ = [
|
|
782
|
+
"analyze_complete",
|
|
783
|
+
]
|