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.
Files changed (59) hide show
  1. oscura/__init__.py +1 -7
  2. oscura/acquisition/__init__.py +147 -0
  3. oscura/acquisition/file.py +255 -0
  4. oscura/acquisition/hardware.py +186 -0
  5. oscura/acquisition/saleae.py +340 -0
  6. oscura/acquisition/socketcan.py +315 -0
  7. oscura/acquisition/streaming.py +38 -0
  8. oscura/acquisition/synthetic.py +229 -0
  9. oscura/acquisition/visa.py +376 -0
  10. oscura/analyzers/__init__.py +3 -0
  11. oscura/analyzers/digital/__init__.py +48 -0
  12. oscura/analyzers/digital/clock.py +9 -1
  13. oscura/analyzers/digital/edges.py +1 -1
  14. oscura/analyzers/digital/extraction.py +195 -0
  15. oscura/analyzers/digital/ic_database.py +498 -0
  16. oscura/analyzers/digital/timing.py +41 -11
  17. oscura/analyzers/digital/timing_paths.py +339 -0
  18. oscura/analyzers/digital/vintage.py +377 -0
  19. oscura/analyzers/digital/vintage_result.py +148 -0
  20. oscura/analyzers/protocols/__init__.py +22 -1
  21. oscura/analyzers/protocols/parallel_bus.py +449 -0
  22. oscura/analyzers/side_channel/__init__.py +52 -0
  23. oscura/analyzers/side_channel/power.py +690 -0
  24. oscura/analyzers/side_channel/timing.py +369 -0
  25. oscura/analyzers/signal_integrity/sparams.py +1 -1
  26. oscura/automotive/__init__.py +4 -2
  27. oscura/automotive/can/patterns.py +3 -1
  28. oscura/automotive/can/session.py +277 -78
  29. oscura/automotive/can/state_machine.py +5 -2
  30. oscura/builders/__init__.py +9 -11
  31. oscura/builders/signal_builder.py +99 -191
  32. oscura/core/exceptions.py +5 -1
  33. oscura/export/__init__.py +12 -0
  34. oscura/export/wavedrom.py +430 -0
  35. oscura/exporters/json_export.py +47 -0
  36. oscura/exporters/vintage_logic_csv.py +247 -0
  37. oscura/loaders/__init__.py +1 -0
  38. oscura/loaders/chipwhisperer.py +393 -0
  39. oscura/loaders/touchstone.py +1 -1
  40. oscura/reporting/__init__.py +7 -0
  41. oscura/reporting/vintage_logic_report.py +523 -0
  42. oscura/session/session.py +54 -46
  43. oscura/sessions/__init__.py +70 -0
  44. oscura/sessions/base.py +323 -0
  45. oscura/sessions/blackbox.py +640 -0
  46. oscura/sessions/generic.py +189 -0
  47. oscura/utils/autodetect.py +5 -1
  48. oscura/visualization/digital_advanced.py +718 -0
  49. oscura/visualization/figure_manager.py +156 -0
  50. {oscura-0.3.0.dist-info → oscura-0.5.0.dist-info}/METADATA +86 -5
  51. {oscura-0.3.0.dist-info → oscura-0.5.0.dist-info}/RECORD +54 -33
  52. oscura/automotive/dtc/data.json +0 -2763
  53. oscura/schemas/bus_configuration.json +0 -322
  54. oscura/schemas/device_mapping.json +0 -182
  55. oscura/schemas/packet_format.json +0 -418
  56. oscura/schemas/protocol_definition.json +0 -363
  57. {oscura-0.3.0.dist-info → oscura-0.5.0.dist-info}/WHEEL +0 -0
  58. {oscura-0.3.0.dist-info → oscura-0.5.0.dist-info}/entry_points.txt +0 -0
  59. {oscura-0.3.0.dist-info → oscura-0.5.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,70 @@
1
+ """Unified session management for Oscura.
2
+
3
+ This module provides the AnalysisSession hierarchy - a unified pattern for
4
+ interactive signal analysis across different domains.
5
+
6
+ All analysis sessions (CAN, Serial, BlackBox, RF, etc.) inherit from
7
+ AnalysisSession and provide consistent interfaces for:
8
+ - Recording management (add, list, compare)
9
+ - Differential analysis
10
+ - Result export
11
+ - Domain-specific analysis methods
12
+
13
+ Example - Generic Session:
14
+ >>> from oscura.sessions import GenericSession
15
+ >>> from oscura.acquisition import FileSource
16
+ >>>
17
+ >>> session = GenericSession()
18
+ >>> session.add_recording("test", FileSource("capture.wfm"))
19
+ >>> results = session.analyze()
20
+ >>> print(results["summary"]["test"]["mean"])
21
+
22
+ Example - Domain-Specific Session:
23
+ >>> from oscura.sessions import AnalysisSession
24
+ >>> from oscura.acquisition import FileSource
25
+ >>>
26
+ >>> class CANSession(AnalysisSession):
27
+ ... def analyze(self):
28
+ ... # CAN-specific signal discovery
29
+ ... return self.discover_signals()
30
+ ...
31
+ ... def discover_signals(self):
32
+ ... # Extract CAN signals from recordings
33
+ ... pass
34
+ >>>
35
+ >>> session = CANSession()
36
+ >>> session.add_recording("baseline", FileSource("idle.blf"))
37
+ >>> signals = session.analyze()
38
+
39
+ Pattern Decision Table:
40
+ - Use GenericSession for general waveform analysis
41
+ - Extend AnalysisSession for domain-specific workflows
42
+ - Use existing session.Session for backward compatibility
43
+
44
+ Architecture:
45
+ Layer 3 (High-Level API) - User-Facing
46
+ ├── AnalysisSession (ABC)
47
+ │ ├── GenericSession
48
+ │ ├── CANSession (Phase 1)
49
+ │ ├── SerialSession (Phase 1)
50
+ │ ├── BlackBoxSession (Phase 1)
51
+ │ └── [Future domain sessions]
52
+ └── [Workflows wrapping sessions]
53
+
54
+ References:
55
+ Architecture Plan Phase 0.3: AnalysisSession Base Class
56
+ docs/architecture/api-patterns.md: When to use Sessions vs Workflows
57
+ """
58
+
59
+ from oscura.sessions.base import AnalysisSession, ComparisonResult
60
+ from oscura.sessions.blackbox import BlackBoxSession, FieldHypothesis, ProtocolSpec
61
+ from oscura.sessions.generic import GenericSession
62
+
63
+ __all__ = [
64
+ "AnalysisSession",
65
+ "BlackBoxSession",
66
+ "ComparisonResult",
67
+ "FieldHypothesis",
68
+ "GenericSession",
69
+ "ProtocolSpec",
70
+ ]
@@ -0,0 +1,323 @@
1
+ """Base class for analysis sessions.
2
+
3
+ This module defines AnalysisSession - the abstract base class for all
4
+ interactive analysis sessions in Oscura. It provides a unified pattern
5
+ for domain-specific sessions (CAN, Serial, BlackBox, etc.).
6
+
7
+ Example:
8
+ >>> from oscura.sessions import AnalysisSession
9
+ >>> from oscura.acquisition import FileSource, HardwareSource
10
+ >>>
11
+ >>> # Domain-specific sessions extend AnalysisSession
12
+ >>> class CANSession(AnalysisSession):
13
+ ... def analyze(self):
14
+ ... # CAN-specific analysis
15
+ ... return self.discover_signals()
16
+ ...
17
+ ... def discover_signals(self):
18
+ ... # Extract CAN signals from recordings
19
+ ... pass
20
+ >>>
21
+ >>> # Unified interface across all sessions
22
+ >>> session = CANSession()
23
+ >>> session.add_recording("baseline", FileSource("idle.blf"))
24
+ >>> session.add_recording("active", FileSource("running.blf"))
25
+ >>> diff = session.compare("baseline", "active")
26
+
27
+ Pattern:
28
+ All domain-specific sessions (CAN, Serial, BlackBox, etc.) inherit
29
+ from AnalysisSession and provide:
30
+ - Recording management (add_recording, list_recordings)
31
+ - Comparison and differential analysis
32
+ - Result export (reports, specs, dissectors)
33
+ - Domain-specific analysis methods (abstract)
34
+
35
+ Benefits:
36
+ - Consistent API across all analysis domains
37
+ - Polymorphic session handling
38
+ - Shared infrastructure (comparison, export, history)
39
+ - Domain-specific specialization via inheritance
40
+
41
+ References:
42
+ Architecture Plan Phase 0.3: AnalysisSession Base Class
43
+ docs/architecture/api-patterns.md: When to use Sessions
44
+ """
45
+
46
+ from __future__ import annotations
47
+
48
+ from abc import ABC, abstractmethod
49
+ from dataclasses import dataclass, field
50
+ from datetime import datetime
51
+ from pathlib import Path
52
+ from typing import TYPE_CHECKING, Any
53
+
54
+ if TYPE_CHECKING:
55
+ from oscura.acquisition import Source
56
+ from oscura.core.types import Trace
57
+
58
+
59
+ @dataclass
60
+ class ComparisonResult:
61
+ """Result of comparing two recordings.
62
+
63
+ Attributes:
64
+ recording1: Name of first recording.
65
+ recording2: Name of second recording.
66
+ changed_bytes: Number of bytes that differ.
67
+ changed_regions: List of (start, end, description) tuples.
68
+ similarity_score: Similarity metric (0.0 to 1.0).
69
+ details: Additional comparison details.
70
+
71
+ Example:
72
+ >>> result = session.compare("baseline", "stimulus")
73
+ >>> print(f"Changed bytes: {result.changed_bytes}")
74
+ >>> print(f"Similarity: {result.similarity_score:.2%}")
75
+ """
76
+
77
+ recording1: str
78
+ recording2: str
79
+ changed_bytes: int
80
+ changed_regions: list[tuple[int, int, str]] = field(default_factory=list)
81
+ similarity_score: float = 0.0
82
+ details: dict[str, Any] = field(default_factory=dict)
83
+
84
+
85
+ class AnalysisSession(ABC):
86
+ """Abstract base class for all analysis sessions.
87
+
88
+ Provides unified interface for interactive signal analysis across
89
+ different domains (CAN, Serial, RF, BlackBox, etc.). All domain-specific
90
+ sessions extend this class.
91
+
92
+ Subclasses must implement:
93
+ - analyze(): Domain-specific analysis method
94
+
95
+ Subclasses may override:
96
+ - export_results(): Custom export formats
97
+ - compare(): Domain-specific comparison logic
98
+
99
+ Attributes:
100
+ name: Session name.
101
+ recordings: Dictionary mapping names to (source, trace) tuples.
102
+ metadata: Session metadata dictionary.
103
+ created_at: Session creation timestamp.
104
+ modified_at: Last modification timestamp.
105
+
106
+ Example:
107
+ >>> # Subclass for domain-specific analysis
108
+ >>> class SerialSession(AnalysisSession):
109
+ ... def analyze(self):
110
+ ... # Detect baud rate, decode UART
111
+ ... baud = self.detect_baud_rate()
112
+ ... frames = self.decode_uart(baud)
113
+ ... return {"baud_rate": baud, "frames": frames}
114
+ ...
115
+ ... def detect_baud_rate(self):
116
+ ... # Domain-specific logic
117
+ ... pass
118
+ >>>
119
+ >>> session = SerialSession()
120
+ >>> session.add_recording("capture", FileSource("uart.wfm"))
121
+ >>> results = session.analyze()
122
+ """
123
+
124
+ def __init__(self, name: str = "Untitled Session") -> None:
125
+ """Initialize analysis session.
126
+
127
+ Args:
128
+ name: Session name (default: "Untitled Session").
129
+
130
+ Example:
131
+ >>> session = CANSession(name="Vehicle Debug Session")
132
+ """
133
+ self.name = name
134
+ self.recordings: dict[str, tuple[Source, Trace | None]] = {}
135
+ self.metadata: dict[str, Any] = {}
136
+ self.created_at = datetime.now()
137
+ self.modified_at = datetime.now()
138
+
139
+ def add_recording(
140
+ self,
141
+ name: str,
142
+ source: Source,
143
+ *,
144
+ load_immediately: bool = True,
145
+ ) -> None:
146
+ """Add a recording to the session.
147
+
148
+ Args:
149
+ name: Name for this recording (e.g., "baseline", "stimulus1").
150
+ source: Source to acquire data from (FileSource, HardwareSource, etc.).
151
+ load_immediately: If True, load trace now. If False, defer loading.
152
+
153
+ Raises:
154
+ ValueError: If name already exists.
155
+
156
+ Example:
157
+ >>> from oscura.acquisition import FileSource
158
+ >>> session.add_recording("baseline", FileSource("idle.blf"))
159
+ >>> session.add_recording("active", FileSource("running.blf"))
160
+ """
161
+ if name in self.recordings:
162
+ raise ValueError(f"Recording '{name}' already exists in session")
163
+
164
+ # Load trace if requested
165
+ trace = source.read() if load_immediately else None
166
+
167
+ self.recordings[name] = (source, trace)
168
+ self.modified_at = datetime.now()
169
+
170
+ def get_recording(self, name: str) -> Trace:
171
+ """Get a recording by name, loading if necessary.
172
+
173
+ Args:
174
+ name: Recording name.
175
+
176
+ Returns:
177
+ Loaded trace.
178
+
179
+ Raises:
180
+ KeyError: If recording not found.
181
+
182
+ Example:
183
+ >>> trace = session.get_recording("baseline")
184
+ >>> print(f"Loaded {len(trace.data)} samples")
185
+ """
186
+ if name not in self.recordings:
187
+ available = list(self.recordings.keys())
188
+ raise KeyError(f"Recording '{name}' not found. Available: {available}")
189
+
190
+ source, trace = self.recordings[name]
191
+
192
+ # Load if not already loaded
193
+ if trace is None:
194
+ trace = source.read()
195
+ self.recordings[name] = (source, trace)
196
+
197
+ return trace
198
+
199
+ def list_recordings(self) -> list[str]:
200
+ """List all recording names in the session.
201
+
202
+ Returns:
203
+ List of recording names.
204
+
205
+ Example:
206
+ >>> session.list_recordings()
207
+ ['baseline', 'stimulus1', 'stimulus2']
208
+ """
209
+ return list(self.recordings.keys())
210
+
211
+ def compare(self, name1: str, name2: str) -> ComparisonResult:
212
+ """Compare two recordings (differential analysis).
213
+
214
+ Default implementation provides basic byte-level comparison.
215
+ Subclasses can override for domain-specific comparison logic.
216
+
217
+ Args:
218
+ name1: First recording name.
219
+ name2: Second recording name.
220
+
221
+ Returns:
222
+ ComparisonResult with differences.
223
+
224
+ Raises:
225
+ KeyError: If recordings not found.
226
+
227
+ Example:
228
+ >>> result = session.compare("baseline", "stimulus")
229
+ >>> print(f"Changed: {result.changed_bytes} bytes")
230
+ >>> print(f"Similarity: {result.similarity_score:.2%}")
231
+ """
232
+ trace1 = self.get_recording(name1)
233
+ trace2 = self.get_recording(name2)
234
+
235
+ # Basic comparison - count differing samples
236
+ import numpy as np
237
+
238
+ from oscura.core.types import IQTrace
239
+
240
+ # Handle IQTrace separately
241
+ if isinstance(trace1, IQTrace) or isinstance(trace2, IQTrace):
242
+ raise TypeError("IQTrace comparison not yet supported in base session")
243
+
244
+ min_len = min(len(trace1.data), len(trace2.data))
245
+ data1 = trace1.data[:min_len]
246
+ data2 = trace2.data[:min_len]
247
+
248
+ # For analog traces, use threshold comparison
249
+ threshold = 0.01 # 1% tolerance
250
+ changed = np.abs(data1 - data2) > threshold
251
+ changed_count = int(np.sum(changed))
252
+
253
+ # Similarity score
254
+ similarity = 1.0 - (changed_count / min_len)
255
+
256
+ return ComparisonResult(
257
+ recording1=name1,
258
+ recording2=name2,
259
+ changed_bytes=changed_count,
260
+ similarity_score=similarity,
261
+ details={
262
+ "trace1_length": len(trace1.data),
263
+ "trace2_length": len(trace2.data),
264
+ "compared_length": min_len,
265
+ },
266
+ )
267
+
268
+ def export_results(self, format: str, path: str | Path) -> None:
269
+ """Export analysis results to file.
270
+
271
+ Default implementation provides basic export. Subclasses should
272
+ override to support domain-specific formats (DBC, Wireshark, etc.).
273
+
274
+ Args:
275
+ format: Export format (e.g., "report", "json", "csv").
276
+ path: Output file path.
277
+
278
+ Raises:
279
+ ValueError: If format not supported.
280
+
281
+ Example:
282
+ >>> session.export_results("report", "analysis.txt")
283
+ >>> session.export_results("json", "results.json")
284
+ """
285
+ path = Path(path)
286
+ path.parent.mkdir(parents=True, exist_ok=True)
287
+
288
+ if format == "report":
289
+ # Basic text report
290
+ with open(path, "w") as f:
291
+ f.write(f"Analysis Session: {self.name}\n")
292
+ f.write(f"Created: {self.created_at}\n")
293
+ f.write(f"Modified: {self.modified_at}\n\n")
294
+ f.write(f"Recordings: {len(self.recordings)}\n")
295
+ for name in self.list_recordings():
296
+ f.write(f" - {name}\n")
297
+ else:
298
+ raise ValueError(f"Unsupported export format: {format}")
299
+
300
+ @abstractmethod
301
+ def analyze(self) -> Any:
302
+ """Perform domain-specific analysis.
303
+
304
+ Subclasses must implement this method to provide domain-specific
305
+ analysis functionality (CAN signal discovery, UART decoding,
306
+ protocol reverse engineering, etc.).
307
+
308
+ Returns:
309
+ Analysis results (format depends on domain).
310
+
311
+ Example:
312
+ >>> class CANSession(AnalysisSession):
313
+ ... def analyze(self):
314
+ ... signals = self.discover_signals()
315
+ ... return {"signals": signals}
316
+ """
317
+
318
+ def __repr__(self) -> str:
319
+ """String representation."""
320
+ return f"{self.__class__.__name__}(name={self.name!r}, recordings={len(self.recordings)})"
321
+
322
+
323
+ __all__ = ["AnalysisSession", "ComparisonResult"]