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.
- oscura/__init__.py +1 -7
- oscura/acquisition/__init__.py +147 -0
- oscura/acquisition/file.py +255 -0
- oscura/acquisition/hardware.py +186 -0
- oscura/acquisition/saleae.py +340 -0
- oscura/acquisition/socketcan.py +315 -0
- oscura/acquisition/streaming.py +38 -0
- oscura/acquisition/synthetic.py +229 -0
- oscura/acquisition/visa.py +376 -0
- oscura/analyzers/__init__.py +3 -0
- oscura/analyzers/digital/clock.py +9 -1
- oscura/analyzers/digital/edges.py +1 -1
- oscura/analyzers/digital/timing.py +41 -11
- oscura/analyzers/packet/payload_extraction.py +2 -4
- oscura/analyzers/packet/stream.py +5 -5
- oscura/analyzers/patterns/__init__.py +4 -3
- oscura/analyzers/patterns/clustering.py +3 -1
- oscura/analyzers/power/ac_power.py +0 -2
- oscura/analyzers/power/basic.py +0 -2
- oscura/analyzers/power/ripple.py +0 -2
- oscura/analyzers/side_channel/__init__.py +52 -0
- oscura/analyzers/side_channel/power.py +690 -0
- oscura/analyzers/side_channel/timing.py +369 -0
- oscura/analyzers/signal_integrity/embedding.py +0 -2
- oscura/analyzers/signal_integrity/sparams.py +28 -206
- oscura/analyzers/spectral/fft.py +0 -2
- oscura/analyzers/statistical/__init__.py +3 -3
- oscura/analyzers/statistical/checksum.py +2 -0
- oscura/analyzers/statistical/classification.py +2 -0
- oscura/analyzers/statistical/entropy.py +11 -9
- oscura/analyzers/statistical/ngrams.py +4 -2
- oscura/api/fluent.py +2 -2
- oscura/automotive/__init__.py +4 -4
- oscura/automotive/can/__init__.py +0 -2
- oscura/automotive/can/patterns.py +3 -1
- oscura/automotive/can/session.py +277 -78
- oscura/automotive/can/state_machine.py +5 -2
- oscura/automotive/dbc/__init__.py +0 -2
- oscura/automotive/dtc/__init__.py +0 -2
- oscura/automotive/dtc/data.json +2763 -0
- oscura/automotive/dtc/database.py +37 -2769
- oscura/automotive/j1939/__init__.py +0 -2
- oscura/automotive/loaders/__init__.py +0 -2
- oscura/automotive/loaders/asc.py +0 -2
- oscura/automotive/loaders/blf.py +0 -2
- oscura/automotive/loaders/csv_can.py +0 -2
- oscura/automotive/obd/__init__.py +0 -2
- oscura/automotive/uds/__init__.py +0 -2
- oscura/automotive/uds/models.py +0 -2
- oscura/builders/__init__.py +9 -11
- oscura/builders/signal_builder.py +99 -191
- oscura/cli/main.py +0 -2
- oscura/cli/shell.py +0 -2
- oscura/config/loader.py +0 -2
- oscura/core/backend_selector.py +1 -1
- oscura/core/correlation.py +0 -2
- oscura/core/exceptions.py +61 -3
- oscura/core/lazy.py +5 -3
- oscura/core/memory_limits.py +0 -2
- oscura/core/numba_backend.py +5 -7
- oscura/core/uncertainty.py +3 -3
- oscura/dsl/interpreter.py +2 -0
- oscura/dsl/parser.py +8 -6
- oscura/exploratory/error_recovery.py +3 -3
- oscura/exploratory/parse.py +2 -0
- oscura/exploratory/recovery.py +2 -0
- oscura/exploratory/sync.py +2 -0
- oscura/export/wireshark/generator.py +1 -1
- oscura/export/wireshark/type_mapping.py +2 -0
- oscura/exporters/hdf5.py +1 -3
- oscura/extensibility/templates.py +0 -8
- oscura/inference/active_learning/lstar.py +2 -4
- oscura/inference/active_learning/observation_table.py +0 -2
- oscura/inference/active_learning/oracle.py +3 -1
- oscura/inference/active_learning/teachers/simulator.py +1 -3
- oscura/inference/alignment.py +2 -0
- oscura/inference/message_format.py +2 -0
- oscura/inference/protocol_dsl.py +7 -5
- oscura/inference/sequences.py +12 -14
- oscura/inference/state_machine.py +2 -0
- oscura/integrations/llm.py +3 -1
- oscura/jupyter/display.py +0 -2
- oscura/loaders/__init__.py +68 -51
- oscura/loaders/chipwhisperer.py +393 -0
- oscura/loaders/pcap.py +1 -1
- oscura/loaders/touchstone.py +221 -0
- oscura/math/arithmetic.py +0 -2
- oscura/optimization/parallel.py +9 -6
- oscura/pipeline/composition.py +0 -2
- oscura/plugins/cli.py +0 -2
- oscura/reporting/comparison.py +0 -2
- oscura/reporting/config.py +1 -1
- oscura/reporting/formatting/emphasis.py +2 -0
- oscura/reporting/formatting/numbers.py +0 -2
- oscura/reporting/output.py +1 -3
- oscura/reporting/sections.py +0 -2
- oscura/search/anomaly.py +2 -0
- oscura/session/session.py +91 -16
- oscura/sessions/__init__.py +70 -0
- oscura/sessions/base.py +323 -0
- oscura/sessions/blackbox.py +640 -0
- oscura/sessions/generic.py +189 -0
- oscura/testing/synthetic.py +2 -0
- oscura/ui/formatters.py +4 -2
- oscura/utils/buffer.py +2 -2
- oscura/utils/lazy.py +5 -5
- oscura/utils/memory_advanced.py +2 -2
- oscura/utils/memory_extensions.py +2 -2
- oscura/visualization/colors.py +0 -2
- oscura/visualization/power.py +2 -0
- oscura/workflows/multi_trace.py +2 -0
- {oscura-0.1.2.dist-info → oscura-0.4.0.dist-info}/METADATA +122 -20
- {oscura-0.1.2.dist-info → oscura-0.4.0.dist-info}/RECORD +116 -98
- {oscura-0.1.2.dist-info → oscura-0.4.0.dist-info}/WHEEL +0 -0
- {oscura-0.1.2.dist-info → oscura-0.4.0.dist-info}/entry_points.txt +0 -0
- {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:
|
|
82
|
-
frequencies:
|
|
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:
|
|
107
|
-
is_anomalous:
|
|
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
|
-
) ->
|
|
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) ->
|
|
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[
|
|
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,
|
|
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
|
-
) ->
|
|
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) ->
|
|
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) ->
|
|
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.
|
oscura/automotive/__init__.py
CHANGED
|
@@ -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
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
__version__ = "0.1.0"
|
|
45
|
+
__version__ = "0.4.0" # pragma: no cover
|
|
46
46
|
|
|
47
47
|
__all__ = [
|
|
48
48
|
"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
|
-
>>>
|
|
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)
|
oscura/automotive/can/session.py
CHANGED
|
@@ -7,8 +7,7 @@ provides discovery-oriented analysis workflows.
|
|
|
7
7
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
|
-
from
|
|
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.
|
|
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
|
|
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
|
-
|
|
48
|
-
|
|
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 -
|
|
66
|
-
>>> session = CANSession
|
|
67
|
-
>>>
|
|
68
|
-
>>>
|
|
69
|
-
>>>
|
|
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,
|
|
83
|
+
def __init__(self, name: str = "CAN Session"):
|
|
73
84
|
"""Initialize CAN session.
|
|
74
85
|
|
|
75
86
|
Args:
|
|
76
|
-
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
109
|
-
|
|
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
|
-
|
|
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
|
-
|
|
304
|
-
|
|
305
|
-
>>>
|
|
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
|
|
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
|
-
>>>
|
|
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
|
-
>>>
|
|
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
|
-
>>>
|
|
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
|
-
>>>
|
|
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
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
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
|
-
>>>
|
|
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
|
|
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(
|