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.
Files changed (38) hide show
  1. oscura/__init__.py +1 -1
  2. oscura/analyzers/eye/__init__.py +5 -1
  3. oscura/analyzers/eye/generation.py +501 -0
  4. oscura/analyzers/jitter/__init__.py +6 -6
  5. oscura/analyzers/jitter/timing.py +419 -0
  6. oscura/analyzers/patterns/__init__.py +28 -0
  7. oscura/analyzers/patterns/reverse_engineering.py +991 -0
  8. oscura/analyzers/power/__init__.py +35 -12
  9. oscura/analyzers/statistics/__init__.py +4 -0
  10. oscura/analyzers/statistics/basic.py +149 -0
  11. oscura/analyzers/statistics/correlation.py +47 -6
  12. oscura/analyzers/waveform/__init__.py +2 -0
  13. oscura/analyzers/waveform/measurements.py +145 -23
  14. oscura/analyzers/waveform/spectral.py +361 -8
  15. oscura/automotive/__init__.py +1 -1
  16. oscura/core/config/loader.py +0 -1
  17. oscura/core/types.py +108 -0
  18. oscura/loaders/__init__.py +12 -4
  19. oscura/loaders/tss.py +456 -0
  20. oscura/reporting/__init__.py +88 -1
  21. oscura/reporting/automation.py +348 -0
  22. oscura/reporting/citations.py +374 -0
  23. oscura/reporting/core.py +54 -0
  24. oscura/reporting/formatting/__init__.py +11 -0
  25. oscura/reporting/formatting/measurements.py +279 -0
  26. oscura/reporting/html.py +57 -0
  27. oscura/reporting/interpretation.py +431 -0
  28. oscura/reporting/summary.py +329 -0
  29. oscura/reporting/visualization.py +542 -0
  30. oscura/visualization/__init__.py +2 -1
  31. oscura/visualization/batch.py +521 -0
  32. oscura/workflows/__init__.py +2 -0
  33. oscura/workflows/waveform.py +783 -0
  34. {oscura-0.6.0.dist-info → oscura-0.8.0.dist-info}/METADATA +37 -19
  35. {oscura-0.6.0.dist-info → oscura-0.8.0.dist-info}/RECORD +38 -26
  36. {oscura-0.6.0.dist-info → oscura-0.8.0.dist-info}/WHEEL +0 -0
  37. {oscura-0.6.0.dist-info → oscura-0.8.0.dist-info}/entry_points.txt +0 -0
  38. {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
+ ]