oscura 0.1.2__py3-none-any.whl → 0.4.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 (116) 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/clock.py +9 -1
  12. oscura/analyzers/digital/edges.py +1 -1
  13. oscura/analyzers/digital/timing.py +41 -11
  14. oscura/analyzers/packet/payload_extraction.py +2 -4
  15. oscura/analyzers/packet/stream.py +5 -5
  16. oscura/analyzers/patterns/__init__.py +4 -3
  17. oscura/analyzers/patterns/clustering.py +3 -1
  18. oscura/analyzers/power/ac_power.py +0 -2
  19. oscura/analyzers/power/basic.py +0 -2
  20. oscura/analyzers/power/ripple.py +0 -2
  21. oscura/analyzers/side_channel/__init__.py +52 -0
  22. oscura/analyzers/side_channel/power.py +690 -0
  23. oscura/analyzers/side_channel/timing.py +369 -0
  24. oscura/analyzers/signal_integrity/embedding.py +0 -2
  25. oscura/analyzers/signal_integrity/sparams.py +28 -206
  26. oscura/analyzers/spectral/fft.py +0 -2
  27. oscura/analyzers/statistical/__init__.py +3 -3
  28. oscura/analyzers/statistical/checksum.py +2 -0
  29. oscura/analyzers/statistical/classification.py +2 -0
  30. oscura/analyzers/statistical/entropy.py +11 -9
  31. oscura/analyzers/statistical/ngrams.py +4 -2
  32. oscura/api/fluent.py +2 -2
  33. oscura/automotive/__init__.py +4 -4
  34. oscura/automotive/can/__init__.py +0 -2
  35. oscura/automotive/can/patterns.py +3 -1
  36. oscura/automotive/can/session.py +277 -78
  37. oscura/automotive/can/state_machine.py +5 -2
  38. oscura/automotive/dbc/__init__.py +0 -2
  39. oscura/automotive/dtc/__init__.py +0 -2
  40. oscura/automotive/dtc/data.json +2763 -0
  41. oscura/automotive/dtc/database.py +37 -2769
  42. oscura/automotive/j1939/__init__.py +0 -2
  43. oscura/automotive/loaders/__init__.py +0 -2
  44. oscura/automotive/loaders/asc.py +0 -2
  45. oscura/automotive/loaders/blf.py +0 -2
  46. oscura/automotive/loaders/csv_can.py +0 -2
  47. oscura/automotive/obd/__init__.py +0 -2
  48. oscura/automotive/uds/__init__.py +0 -2
  49. oscura/automotive/uds/models.py +0 -2
  50. oscura/builders/__init__.py +9 -11
  51. oscura/builders/signal_builder.py +99 -191
  52. oscura/cli/main.py +0 -2
  53. oscura/cli/shell.py +0 -2
  54. oscura/config/loader.py +0 -2
  55. oscura/core/backend_selector.py +1 -1
  56. oscura/core/correlation.py +0 -2
  57. oscura/core/exceptions.py +61 -3
  58. oscura/core/lazy.py +5 -3
  59. oscura/core/memory_limits.py +0 -2
  60. oscura/core/numba_backend.py +5 -7
  61. oscura/core/uncertainty.py +3 -3
  62. oscura/dsl/interpreter.py +2 -0
  63. oscura/dsl/parser.py +8 -6
  64. oscura/exploratory/error_recovery.py +3 -3
  65. oscura/exploratory/parse.py +2 -0
  66. oscura/exploratory/recovery.py +2 -0
  67. oscura/exploratory/sync.py +2 -0
  68. oscura/export/wireshark/generator.py +1 -1
  69. oscura/export/wireshark/type_mapping.py +2 -0
  70. oscura/exporters/hdf5.py +1 -3
  71. oscura/extensibility/templates.py +0 -8
  72. oscura/inference/active_learning/lstar.py +2 -4
  73. oscura/inference/active_learning/observation_table.py +0 -2
  74. oscura/inference/active_learning/oracle.py +3 -1
  75. oscura/inference/active_learning/teachers/simulator.py +1 -3
  76. oscura/inference/alignment.py +2 -0
  77. oscura/inference/message_format.py +2 -0
  78. oscura/inference/protocol_dsl.py +7 -5
  79. oscura/inference/sequences.py +12 -14
  80. oscura/inference/state_machine.py +2 -0
  81. oscura/integrations/llm.py +3 -1
  82. oscura/jupyter/display.py +0 -2
  83. oscura/loaders/__init__.py +68 -51
  84. oscura/loaders/chipwhisperer.py +393 -0
  85. oscura/loaders/pcap.py +1 -1
  86. oscura/loaders/touchstone.py +221 -0
  87. oscura/math/arithmetic.py +0 -2
  88. oscura/optimization/parallel.py +9 -6
  89. oscura/pipeline/composition.py +0 -2
  90. oscura/plugins/cli.py +0 -2
  91. oscura/reporting/comparison.py +0 -2
  92. oscura/reporting/config.py +1 -1
  93. oscura/reporting/formatting/emphasis.py +2 -0
  94. oscura/reporting/formatting/numbers.py +0 -2
  95. oscura/reporting/output.py +1 -3
  96. oscura/reporting/sections.py +0 -2
  97. oscura/search/anomaly.py +2 -0
  98. oscura/session/session.py +91 -16
  99. oscura/sessions/__init__.py +70 -0
  100. oscura/sessions/base.py +323 -0
  101. oscura/sessions/blackbox.py +640 -0
  102. oscura/sessions/generic.py +189 -0
  103. oscura/testing/synthetic.py +2 -0
  104. oscura/ui/formatters.py +4 -2
  105. oscura/utils/buffer.py +2 -2
  106. oscura/utils/lazy.py +5 -5
  107. oscura/utils/memory_advanced.py +2 -2
  108. oscura/utils/memory_extensions.py +2 -2
  109. oscura/visualization/colors.py +0 -2
  110. oscura/visualization/power.py +2 -0
  111. oscura/workflows/multi_trace.py +2 -0
  112. {oscura-0.1.2.dist-info → oscura-0.4.0.dist-info}/METADATA +122 -20
  113. {oscura-0.1.2.dist-info → oscura-0.4.0.dist-info}/RECORD +116 -98
  114. {oscura-0.1.2.dist-info → oscura-0.4.0.dist-info}/WHEEL +0 -0
  115. {oscura-0.1.2.dist-info → oscura-0.4.0.dist-info}/entry_points.txt +0 -0
  116. {oscura-0.1.2.dist-info → oscura-0.4.0.dist-info}/licenses/LICENSE +0 -0
@@ -8,6 +8,8 @@ transitions for field boundary identification, and classifying data types
8
8
  based on entropy characteristics.
9
9
  """
10
10
 
11
+ from __future__ import annotations
12
+
11
13
  from collections import Counter
12
14
  from dataclasses import dataclass, field
13
15
  from typing import TYPE_CHECKING, Literal, Union
@@ -78,8 +80,8 @@ class ByteFrequencyResult:
78
80
  printable_ratio: Proportion of printable ASCII.
79
81
  """
80
82
 
81
- counts: "NDArray[np.int64]"
82
- frequencies: "NDArray[np.float64]"
83
+ counts: NDArray[np.int64]
84
+ frequencies: NDArray[np.float64]
83
85
  entropy: float
84
86
  unique_bytes: int
85
87
  most_common: list[tuple[int, int]]
@@ -103,8 +105,8 @@ class FrequencyAnomalyResult:
103
105
  """
104
106
 
105
107
  anomalous_bytes: list[int]
106
- z_scores: "NDArray[np.float64]"
107
- is_anomalous: "NDArray[np.bool_]"
108
+ z_scores: NDArray[np.float64]
109
+ is_anomalous: NDArray[np.bool_]
108
110
  expected_frequency: float
109
111
 
110
112
 
@@ -221,7 +223,7 @@ def bit_entropy(data: DataType) -> float:
221
223
 
222
224
  def sliding_entropy(
223
225
  data: DataType, window: int = 256, step: int = 64, window_size: int | None = None
224
- ) -> "NDArray[np.float64]":
226
+ ) -> NDArray[np.float64]:
225
227
  """Calculate sliding window entropy profile.
226
228
 
227
229
  : Shannon Entropy Analysis
@@ -563,7 +565,7 @@ def classify_by_entropy(data: DataType) -> EntropyResult:
563
565
  )
564
566
 
565
567
 
566
- def entropy_profile(data: DataType, window: int = 256) -> "NDArray[np.float64]":
568
+ def entropy_profile(data: DataType, window: int = 256) -> NDArray[np.float64]:
567
569
  """Generate entropy profile for visualization.
568
570
 
569
571
  : Shannon Entropy Analysis
@@ -588,7 +590,7 @@ def entropy_profile(data: DataType, window: int = 256) -> "NDArray[np.float64]":
588
590
  return sliding_entropy(data, window=window, step=step)
589
591
 
590
592
 
591
- def entropy_histogram(data: DataType) -> tuple["NDArray[np.intp]", "NDArray[np.float64]"]:
593
+ def entropy_histogram(data: DataType) -> tuple[NDArray[np.intp], NDArray[np.float64]]:
592
594
  """Generate byte frequency histogram.
593
595
 
594
596
  : Shannon Entropy Analysis
@@ -799,7 +801,7 @@ def detect_frequency_anomalies(data: DataType, z_threshold: float = 3.0) -> Freq
799
801
 
800
802
  def compare_byte_distributions(
801
803
  data_a: DataType, data_b: DataType
802
- ) -> tuple[float, float, "NDArray[np.float64]"]:
804
+ ) -> tuple[float, float, NDArray[np.float64]]:
803
805
  """Compare byte frequency distributions between two data samples.
804
806
 
805
807
  Implements RE-ENT-002: Byte Frequency Distribution.
@@ -849,7 +851,7 @@ def compare_byte_distributions(
849
851
 
850
852
  def sliding_byte_frequency(
851
853
  data: DataType, window: int = 256, step: int = 64, byte_value: int | None = None
852
- ) -> "NDArray[np.float64]":
854
+ ) -> NDArray[np.float64]:
853
855
  """Compute sliding window byte frequency profile.
854
856
 
855
857
  Implements RE-ENT-002: Byte Frequency Distribution.
@@ -6,6 +6,8 @@ in binary data, useful for pattern identification, data characterization,
6
6
  and protocol fingerprinting.
7
7
  """
8
8
 
9
+ from __future__ import annotations
10
+
9
11
  from collections import Counter
10
12
  from dataclasses import dataclass
11
13
  from typing import TYPE_CHECKING, Any, Union
@@ -310,7 +312,7 @@ def find_unusual_ngrams(
310
312
  return unusual
311
313
 
312
314
 
313
- def ngram_heatmap(data: DataType, n: int = 2) -> "NDArray[np.float64]":
315
+ def ngram_heatmap(data: DataType, n: int = 2) -> NDArray[np.float64]:
314
316
  """Generate n-gram co-occurrence heatmap.
315
317
 
316
318
  : N-gram Frequency Analysis
@@ -586,7 +588,7 @@ class NGramAnalyzer:
586
588
  """
587
589
  return find_unusual_ngrams(data, baseline=baseline, n=self.n, z_threshold=z_threshold)
588
590
 
589
- def heatmap(self, data: DataType) -> "NDArray[np.float64]":
591
+ def heatmap(self, data: DataType) -> NDArray[np.float64]:
590
592
  """Generate bigram heatmap.
591
593
 
592
594
  Args:
oscura/api/fluent.py CHANGED
@@ -7,7 +7,7 @@ expressing signal analysis operations in a readable, intuitive way.
7
7
  from __future__ import annotations
8
8
 
9
9
  from dataclasses import dataclass, field
10
- from typing import TYPE_CHECKING, Any, TypeVar
10
+ from typing import TYPE_CHECKING, Any, Generic, TypeVar
11
11
 
12
12
  import numpy as np
13
13
 
@@ -26,7 +26,7 @@ __all__ = [
26
26
 
27
27
 
28
28
  @dataclass
29
- class FluentResult[T]:
29
+ class FluentResult(Generic[T]):
30
30
  """Result container with fluent interface.
31
31
 
32
32
  Provides method chaining for result processing.
@@ -17,8 +17,10 @@ Key features:
17
17
 
18
18
  Example:
19
19
  >>> from oscura.automotive.can import CANSession
20
+ >>> from oscura.automotive.sources import FileSource
20
21
  >>> # Load automotive log file
21
- >>> session = CANSession.from_log("capture.blf")
22
+ >>> session = CANSession(name="Analysis")
23
+ >>> session.add_recording("main", FileSource("capture.blf"))
22
24
  >>> # View message inventory
23
25
  >>> inventory = session.inventory()
24
26
  >>> # Analyze specific message
@@ -40,9 +42,7 @@ Example:
40
42
  P0420: Catalyst System Efficiency Below Threshold (Bank 1)
41
43
  """
42
44
 
43
- from __future__ import annotations
44
-
45
- __version__ = "0.1.0"
45
+ __version__ = "0.4.0" # pragma: no cover
46
46
 
47
47
  __all__ = [
48
48
  "CANMessage",
@@ -4,8 +4,6 @@ This submodule provides CAN-specific analysis tools for reverse engineering
4
4
  automotive protocols from captured CAN bus data.
5
5
  """
6
6
 
7
- from __future__ import annotations
8
-
9
7
  __all__ = [
10
8
  "ByteChange",
11
9
  "CANMessage",
@@ -113,7 +113,9 @@ class PatternAnalyzer:
113
113
  CAN messages, useful for understanding message dependencies and control flows.
114
114
 
115
115
  Example - Find message pairs:
116
- >>> session = CANSession.from_log("capture.blf")
116
+ >>> from oscura.automotive.sources import FileSource
117
+ >>> session = CANSession(name="Analysis")
118
+ >>> session.add_recording("main", FileSource("capture.blf"))
117
119
  >>> pairs = PatternAnalyzer.find_message_pairs(session, time_window_ms=100)
118
120
  >>> for pair in pairs:
119
121
  ... print(pair)
@@ -7,8 +7,7 @@ provides discovery-oriented analysis workflows.
7
7
 
8
8
  from __future__ import annotations
9
9
 
10
- from pathlib import Path
11
- from typing import TYPE_CHECKING
10
+ from typing import TYPE_CHECKING, Any
12
11
 
13
12
  import pandas as pd
14
13
 
@@ -18,42 +17,53 @@ from oscura.automotive.can.models import (
18
17
  CANMessageList,
19
18
  MessageAnalysis,
20
19
  )
21
- from oscura.automotive.can.patterns import (
22
- MessagePair,
23
- MessageSequence,
24
- PatternAnalyzer,
25
- TemporalCorrelation,
26
- )
20
+ from oscura.sessions.base import AnalysisSession, ComparisonResult
27
21
 
28
22
  if TYPE_CHECKING:
29
23
  from oscura.automotive.can.message_wrapper import CANMessageWrapper
24
+ from oscura.automotive.can.patterns import (
25
+ MessagePair,
26
+ MessageSequence,
27
+ TemporalCorrelation,
28
+ )
30
29
  from oscura.automotive.can.stimulus_response import StimulusResponseReport
31
30
  from oscura.inference.state_machine import FiniteAutomaton
32
31
 
33
32
  __all__ = ["CANSession"]
34
33
 
35
34
 
36
- class CANSession:
35
+ class CANSession(AnalysisSession):
37
36
  """CAN bus reverse engineering session.
38
37
 
39
38
  This is the primary API for discovering and analyzing unknown CAN bus
40
- protocols. It provides:
39
+ protocols. It extends AnalysisSession to provide unified interface for
40
+ multi-session workflows with CAN-specific functionality.
41
+
42
+ Features:
43
+ - Recording management (add/remove/compare recordings)
41
44
  - Message inventory and filtering
42
45
  - Per-message statistical analysis
43
46
  - Discovery-oriented workflows
44
47
  - Hypothesis testing
45
48
  - Documentation generation
46
-
47
- Example - Discovery workflow:
48
- >>> session = CANSession.from_log("capture.blf")
49
+ - Pattern discovery (pairs, sequences, correlations)
50
+ - State machine inference
51
+
52
+ Example - Basic usage:
53
+ >>> from oscura.sessions import CANSession
54
+ >>> from oscura.acquisition import FileSource
55
+ >>> session = CANSession(name="Vehicle Analysis")
56
+ >>> session.add_recording("baseline", FileSource("idle.blf"))
49
57
  >>> inventory = session.inventory()
50
58
  >>> print(inventory)
51
- >>>
59
+
60
+ Example - Discovery workflow:
61
+ >>> session = CANSession(name="Brake Analysis")
62
+ >>> session.add_recording("data", FileSource("capture.blf"))
52
63
  >>> # Focus on a specific message
53
64
  >>> msg = session.message(0x280)
54
65
  >>> analysis = msg.analyze()
55
66
  >>> print(analysis.summary())
56
- >>>
57
67
  >>> # Test hypothesis
58
68
  >>> hypothesis = msg.test_hypothesis(
59
69
  ... signal_name="rpm",
@@ -62,51 +72,31 @@ class CANSession:
62
72
  ... scale=0.25
63
73
  ... )
64
74
 
65
- Example - Known protocol decoding:
66
- >>> session = CANSession.from_log("capture.blf")
67
- >>> from oscura.automotive.dbc import load_dbc
68
- >>> dbc = load_dbc("vehicle.dbc")
69
- >>> decoded = session.decode(dbc)
75
+ Example - Compare recordings:
76
+ >>> session = CANSession(name="Brake Analysis")
77
+ >>> session.add_recording("no_brake", FileSource("idle.blf"))
78
+ >>> session.add_recording("brake_pressed", FileSource("brake.blf"))
79
+ >>> result = session.compare("no_brake", "brake_pressed")
80
+ >>> print(f"Changed messages: {result.changed_bytes}")
70
81
  """
71
82
 
72
- def __init__(self, messages: CANMessageList | None = None):
83
+ def __init__(self, name: str = "CAN Session"):
73
84
  """Initialize CAN session.
74
85
 
75
86
  Args:
76
- messages: Initial message collection (optional).
77
- """
78
- self._messages = messages or CANMessageList()
79
- self._analyses_cache: dict[int, MessageAnalysis] = {}
80
-
81
- @classmethod
82
- def from_log(cls, file_path: Path | str) -> CANSession:
83
- """Create session from automotive log file.
84
-
85
- Automatically detects file format (BLF, ASC, MDF, CSV) and loads.
86
-
87
- Args:
88
- file_path: Path to log file.
89
-
90
- Returns:
91
- New CANSession with loaded messages.
92
- """
93
- from oscura.automotive.loaders import load_automotive_log
94
-
95
- messages = load_automotive_log(file_path)
96
- return cls(messages=messages)
97
-
98
- @classmethod
99
- def from_messages(cls, messages: list[CANMessage]) -> CANSession:
100
- """Create session from list of CAN messages.
87
+ name: Session name (default: "CAN Session").
101
88
 
102
- Args:
103
- messages: List of CAN messages.
104
-
105
- Returns:
106
- New CANSession.
89
+ Example:
90
+ >>> from oscura.sessions import CANSession
91
+ >>> from oscura.acquisition import FileSource
92
+ >>> session = CANSession(name="Vehicle Analysis")
93
+ >>> session.add_recording("baseline", FileSource("idle.blf"))
94
+ >>> session.add_recording("active", FileSource("running.blf"))
95
+ >>> results = session.analyze()
107
96
  """
108
- msg_list = CANMessageList(messages=messages)
109
- return cls(messages=msg_list)
97
+ super().__init__(name=name)
98
+ self._messages = CANMessageList()
99
+ self._analyses_cache: dict[int, MessageAnalysis] = {}
110
100
 
111
101
  def inventory(self) -> pd.DataFrame:
112
102
  """Generate message inventory.
@@ -218,6 +208,11 @@ class CANSession:
218
208
 
219
209
  Returns:
220
210
  New CANSession with filtered messages.
211
+
212
+ Note:
213
+ This creates a new session with filtered messages from the current
214
+ internal message collection. This method is primarily for legacy
215
+ workflows. For new code, use add_recording() with separate files.
221
216
  """
222
217
  filtered_messages = []
223
218
 
@@ -265,7 +260,10 @@ class CANSession:
265
260
  msg for msg in filtered_messages if msg.arbitration_id in valid_ids
266
261
  ]
267
262
 
268
- return CANSession.from_messages(filtered_messages)
263
+ # Create new session with filtered messages
264
+ new_session = CANSession(name=f"{self.name} (filtered)")
265
+ new_session._messages = CANMessageList(messages=filtered_messages)
266
+ return new_session
269
267
 
270
268
  def unique_ids(self) -> set[int]:
271
269
  """Get set of unique CAN IDs in this session.
@@ -287,6 +285,185 @@ class CANSession:
287
285
  """Return total number of messages."""
288
286
  return len(self._messages)
289
287
 
288
+ def analyze(self) -> dict[str, Any]:
289
+ """Perform comprehensive CAN protocol analysis.
290
+
291
+ Implements the AnalysisSession abstract method. Analyzes all messages
292
+ in the current session to discover signals, patterns, and protocol structure.
293
+
294
+ Returns:
295
+ Dictionary with analysis results:
296
+ - inventory: Message inventory DataFrame
297
+ - num_messages: Total number of messages
298
+ - num_unique_ids: Number of unique CAN IDs
299
+ - time_range: Tuple of (start, end) timestamps
300
+ - message_analyses: Dict mapping CAN ID to MessageAnalysis
301
+ - patterns: Discovered patterns (pairs, sequences, correlations)
302
+
303
+ Example:
304
+ >>> from oscura.sessions import CANSession
305
+ >>> from oscura.acquisition import FileSource
306
+ >>> session = CANSession(name="Analysis")
307
+ >>> session.add_recording("data", FileSource("capture.blf"))
308
+ >>> results = session.analyze()
309
+ >>> print(f"Found {results['num_unique_ids']} unique CAN IDs")
310
+ >>> print(f"Duration: {results['time_range'][1] - results['time_range'][0]:.2f}s")
311
+
312
+ Note:
313
+ This is the unified AnalysisSession interface. For CAN-specific
314
+ workflows, use inventory(), message(), and other domain methods.
315
+ """
316
+ # Generate inventory
317
+ inventory = self.inventory()
318
+
319
+ # Analyze all unique IDs
320
+ message_analyses = {}
321
+ for arb_id in self.unique_ids():
322
+ try:
323
+ analysis = self.analyze_message(arb_id)
324
+ message_analyses[arb_id] = analysis
325
+ except Exception:
326
+ # Skip messages that fail analysis
327
+ continue
328
+
329
+ # Find patterns (if enough messages)
330
+ patterns: dict[str, Any] = {}
331
+ if len(self._messages) >= 10:
332
+ try:
333
+ patterns["message_pairs"] = self.find_message_pairs(
334
+ time_window_ms=100, min_occurrence=3
335
+ )
336
+ patterns["temporal_correlations"] = self.find_temporal_correlations(
337
+ max_delay_ms=100
338
+ )
339
+ except Exception:
340
+ # Pattern analysis is optional
341
+ pass
342
+
343
+ return {
344
+ "inventory": inventory,
345
+ "num_messages": len(self._messages),
346
+ "num_unique_ids": len(self.unique_ids()),
347
+ "time_range": self.time_range() if len(self._messages) > 0 else (0.0, 0.0),
348
+ "message_analyses": message_analyses,
349
+ "patterns": patterns,
350
+ }
351
+
352
+ def compare(self, name1: str, name2: str) -> ComparisonResult:
353
+ """Compare two CAN recordings (stimulus-response analysis).
354
+
355
+ Overrides AnalysisSession.compare() to provide CAN-specific differential
356
+ analysis. Compares two recordings to detect changed messages, byte-level
357
+ differences, and signal variations.
358
+
359
+ Args:
360
+ name1: Name of first recording (baseline).
361
+ name2: Name of second recording (stimulus).
362
+
363
+ Returns:
364
+ ComparisonResult with CAN-specific differences:
365
+ - changed_bytes: Number of message-bytes that differ
366
+ - changed_regions: List of (message_id, byte_offset, description)
367
+ - similarity_score: Overall similarity (0.0 to 1.0)
368
+ - details: CAN-specific details (changed_ids, byte_changes, etc.)
369
+
370
+ Example:
371
+ >>> from oscura.acquisition import FileSource
372
+ >>> session = CANSession(name="Brake Analysis")
373
+ >>> session.add_recording("no_brake", FileSource("idle.blf"))
374
+ >>> session.add_recording("brake_pressed", FileSource("brake.blf"))
375
+ >>> result = session.compare("no_brake", "brake_pressed")
376
+ >>> print(f"Changed messages: {result.details['changed_message_ids']}")
377
+
378
+ Note:
379
+ This uses the unified AnalysisSession interface. For advanced
380
+ CAN-specific comparison, use compare_to() method.
381
+ """
382
+ # Load recordings as CANSession instances
383
+ recording1 = self._recording_to_session(name1)
384
+ recording2 = self._recording_to_session(name2)
385
+
386
+ # Use CAN-specific stimulus-response analysis
387
+ from oscura.automotive.can.stimulus_response import StimulusResponseAnalyzer
388
+
389
+ analyzer = StimulusResponseAnalyzer()
390
+ report = analyzer.detect_responses(recording1, recording2)
391
+
392
+ # Convert to ComparisonResult
393
+ changed_message_ids = report.changed_messages
394
+ total_byte_changes = sum(len(changes) for changes in report.byte_changes.values())
395
+
396
+ # Build changed regions list (message_id, byte_offset, description)
397
+ changed_regions = []
398
+ for msg_id, changes in report.byte_changes.items():
399
+ for change in changes:
400
+ changed_regions.append(
401
+ (
402
+ msg_id,
403
+ change.byte_position,
404
+ f"Magnitude: {change.change_magnitude:.2f}",
405
+ )
406
+ )
407
+
408
+ # Calculate similarity (1.0 = identical, 0.0 = completely different)
409
+ total_unique_ids = len(recording1.unique_ids().union(recording2.unique_ids()))
410
+ if total_unique_ids > 0:
411
+ similarity = 1.0 - (len(changed_message_ids) / total_unique_ids)
412
+ else:
413
+ similarity = 1.0
414
+
415
+ return ComparisonResult(
416
+ recording1=name1,
417
+ recording2=name2,
418
+ changed_bytes=total_byte_changes,
419
+ changed_regions=changed_regions, # type: ignore[arg-type]
420
+ similarity_score=similarity,
421
+ details={
422
+ "changed_message_ids": changed_message_ids,
423
+ "byte_changes": report.byte_changes,
424
+ "new_messages": report.new_messages,
425
+ "disappeared_messages": report.disappeared_messages,
426
+ "stimulus_response_report": report,
427
+ },
428
+ )
429
+
430
+ def _recording_to_session(self, name: str) -> CANSession:
431
+ """Convert a recording to a CANSession instance.
432
+
433
+ Args:
434
+ name: Recording name.
435
+
436
+ Returns:
437
+ CANSession loaded from the recording.
438
+
439
+ Raises:
440
+ KeyError: If recording not found.
441
+ ValueError: If recording is not a valid CAN log file.
442
+ """
443
+ if name not in self.recordings:
444
+ available = list(self.recordings.keys())
445
+ raise KeyError(f"Recording '{name}' not found. Available: {available}")
446
+
447
+ source, _ = self.recordings[name]
448
+
449
+ # Get file path from source and load messages
450
+ # FileSource has a 'path' attribute
451
+ if hasattr(source, "path"):
452
+ from oscura.automotive.loaders import load_automotive_log
453
+
454
+ file_path = source.path
455
+ messages = load_automotive_log(file_path)
456
+
457
+ # Create new session and populate with messages
458
+ session = CANSession(name=name)
459
+ session._messages = messages
460
+ return session
461
+ else:
462
+ raise ValueError(
463
+ f"Recording '{name}' is not from a file source. "
464
+ "Recording-based comparison requires FileSource."
465
+ )
466
+
290
467
  def compare_to(self, other_session: CANSession) -> StimulusResponseReport:
291
468
  """Compare this session to another to detect changes.
292
469
 
@@ -300,24 +477,22 @@ class CANSession:
300
477
  Returns:
301
478
  StimulusResponseReport with detected changes.
302
479
 
303
- Example - Brake pedal analysis:
304
- >>> baseline = CANSession.from_log("no_brake.blf")
305
- >>> stimulus = CANSession.from_log("brake_pressed.blf")
480
+ Note:
481
+ For comparing recordings within a session, use compare() instead:
482
+ >>> session.compare("baseline", "stimulus")
483
+
484
+ This method is for comparing two separate CANSession instances.
485
+
486
+ Example:
487
+ >>> # Compare two session instances directly
488
+ >>> baseline = CANSession(name="Baseline")
489
+ >>> stimulus = CANSession(name="Stimulus")
490
+ >>> # ... populate sessions with messages ...
306
491
  >>> report = baseline.compare_to(stimulus)
307
492
  >>> print(report.summary())
308
493
  >>> # Show which messages changed
309
494
  >>> for msg_id in report.changed_messages:
310
- ... print(f"0x{msg_id:03X} responded to brake press")
311
-
312
- Example - Throttle position analysis:
313
- >>> idle = CANSession.from_log("idle.blf")
314
- >>> throttle = CANSession.from_log("throttle_50pct.blf")
315
- >>> report = idle.compare_to(throttle)
316
- >>> # Examine byte-level changes
317
- >>> for msg_id, changes in report.byte_changes.items():
318
- ... print(f"Message 0x{msg_id:03X}:")
319
- ... for change in changes:
320
- ... print(f" Byte {change.byte_position}: {change.change_magnitude:.2f}")
495
+ ... print(f"0x{msg_id:03X} responded")
321
496
  """
322
497
  from oscura.automotive.can.stimulus_response import (
323
498
  StimulusResponseAnalyzer,
@@ -344,11 +519,16 @@ class CANSession:
344
519
  List of MessagePair objects, sorted by occurrence count.
345
520
 
346
521
  Example:
347
- >>> session = CANSession.from_log("capture.blf")
522
+ >>> from oscura.sessions import CANSession
523
+ >>> from oscura.acquisition import FileSource
524
+ >>> session = CANSession(name="Pattern Analysis")
525
+ >>> session.add_recording("data", FileSource("capture.blf"))
348
526
  >>> pairs = session.find_message_pairs(time_window_ms=50)
349
527
  >>> for pair in pairs[:5]:
350
528
  ... print(pair)
351
529
  """
530
+ from oscura.automotive.can.patterns import PatternAnalyzer
531
+
352
532
  return PatternAnalyzer.find_message_pairs(
353
533
  self, time_window_ms=time_window_ms, min_occurrence=min_occurrence
354
534
  )
@@ -373,7 +553,10 @@ class CANSession:
373
553
  List of MessageSequence objects, sorted by support.
374
554
 
375
555
  Example:
376
- >>> session = CANSession.from_log("startup.blf")
556
+ >>> from oscura.sessions import CANSession
557
+ >>> from oscura.acquisition import FileSource
558
+ >>> session = CANSession(name="Sequence Analysis")
559
+ >>> session.add_recording("data", FileSource("startup.blf"))
377
560
  >>> sequences = session.find_message_sequences(
378
561
  ... max_sequence_length=3,
379
562
  ... time_window_ms=1000
@@ -381,6 +564,8 @@ class CANSession:
381
564
  >>> for seq in sequences[:5]:
382
565
  ... print(seq)
383
566
  """
567
+ from oscura.automotive.can.patterns import PatternAnalyzer
568
+
384
569
  return PatternAnalyzer.find_message_sequences(
385
570
  self,
386
571
  max_sequence_length=max_sequence_length,
@@ -404,11 +589,16 @@ class CANSession:
404
589
  Dictionary mapping (leader_id, follower_id) to correlation info.
405
590
 
406
591
  Example:
407
- >>> session = CANSession.from_log("capture.blf")
592
+ >>> from oscura.sessions import CANSession
593
+ >>> from oscura.acquisition import FileSource
594
+ >>> session = CANSession(name="Correlation Analysis")
595
+ >>> session.add_recording("data", FileSource("capture.blf"))
408
596
  >>> correlations = session.find_temporal_correlations(max_delay_ms=50)
409
597
  >>> for (leader, follower), corr in correlations.items():
410
598
  ... print(f"0x{leader:03X} → 0x{follower:03X}: {corr.avg_delay_ms:.2f}ms")
411
599
  """
600
+ from oscura.automotive.can.patterns import PatternAnalyzer
601
+
412
602
  return PatternAnalyzer.find_temporal_correlations(self, max_delay_ms=max_delay_ms)
413
603
 
414
604
  def learn_state_machine(
@@ -427,7 +617,10 @@ class CANSession:
427
617
  Learned finite automaton representing the state machine.
428
618
 
429
619
  Example:
430
- >>> session = CANSession.from_log("ignition_cycles.blf")
620
+ >>> from oscura.sessions import CANSession
621
+ >>> from oscura.acquisition import FileSource
622
+ >>> session = CANSession(name="State Machine Learning")
623
+ >>> session.add_recording("data", FileSource("ignition_cycles.blf"))
431
624
  >>> automaton = session.learn_state_machine(
432
625
  ... trigger_ids=[0x280],
433
626
  ... context_window_ms=500
@@ -444,9 +637,15 @@ class CANSession:
444
637
  """Human-readable representation."""
445
638
  num_messages = len(self._messages)
446
639
  num_ids = len(self.unique_ids())
447
- time_start, time_end = self.time_range()
448
- duration = time_end - time_start
449
-
450
- return (
451
- f"CANSession({num_messages} messages, {num_ids} unique IDs, duration={duration:.2f}s)"
452
- )
640
+ num_recordings = len(self.recordings)
641
+
642
+ if num_messages > 0:
643
+ time_start, time_end = self.time_range()
644
+ duration = time_end - time_start
645
+ return (
646
+ f"CANSession(name={self.name!r}, {num_messages} messages, "
647
+ f"{num_ids} unique IDs, duration={duration:.2f}s, "
648
+ f"recordings={num_recordings})"
649
+ )
650
+ else:
651
+ return f"CANSession(name={self.name!r}, recordings={num_recordings})"
@@ -73,7 +73,9 @@ class CANStateMachine:
73
73
  - State-dependent message patterns
74
74
 
75
75
  Example - Learn ignition sequence:
76
- >>> session = CANSession.from_log("ignition_cycles.blf")
76
+ >>> from oscura.automotive.sources import FileSource
77
+ >>> session = CANSession(name="Ignition Analysis")
78
+ >>> session.add_recording("cycles", FileSource("ignition_cycles.blf"))
77
79
  >>> sm = CANStateMachine()
78
80
  >>> # Use ignition-related CAN IDs as triggers
79
81
  >>> automaton = sm.learn_from_session(
@@ -85,7 +87,8 @@ class CANStateMachine:
85
87
  >>> print(automaton.to_dot())
86
88
 
87
89
  Example - Discover initialization sequence:
88
- >>> session = CANSession.from_log("ecu_startup.blf")
90
+ >>> session = CANSession(name="ECU Startup")
91
+ >>> session.add_recording("startup", FileSource("ecu_startup.blf"))
89
92
  >>> sm = CANStateMachine()
90
93
  >>> # Use diagnostic messages as triggers
91
94
  >>> automaton = sm.learn_from_session(
@@ -3,8 +3,6 @@
3
3
  This module provides DBC file parsing and generation capabilities.
4
4
  """
5
5
 
6
- from __future__ import annotations
7
-
8
6
  __all__ = ["DBCGenerator", "DBCParser", "load_dbc"]
9
7
 
10
8
  try: