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.
- 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/__init__.py +48 -0
- oscura/analyzers/digital/clock.py +9 -1
- oscura/analyzers/digital/edges.py +1 -1
- oscura/analyzers/digital/extraction.py +195 -0
- oscura/analyzers/digital/ic_database.py +498 -0
- oscura/analyzers/digital/timing.py +41 -11
- oscura/analyzers/digital/timing_paths.py +339 -0
- oscura/analyzers/digital/vintage.py +377 -0
- oscura/analyzers/digital/vintage_result.py +148 -0
- oscura/analyzers/protocols/__init__.py +22 -1
- oscura/analyzers/protocols/parallel_bus.py +449 -0
- 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/sparams.py +1 -1
- oscura/automotive/__init__.py +4 -2
- oscura/automotive/can/patterns.py +3 -1
- oscura/automotive/can/session.py +277 -78
- oscura/automotive/can/state_machine.py +5 -2
- oscura/builders/__init__.py +9 -11
- oscura/builders/signal_builder.py +99 -191
- oscura/core/exceptions.py +5 -1
- oscura/export/__init__.py +12 -0
- oscura/export/wavedrom.py +430 -0
- oscura/exporters/json_export.py +47 -0
- oscura/exporters/vintage_logic_csv.py +247 -0
- oscura/loaders/__init__.py +1 -0
- oscura/loaders/chipwhisperer.py +393 -0
- oscura/loaders/touchstone.py +1 -1
- oscura/reporting/__init__.py +7 -0
- oscura/reporting/vintage_logic_report.py +523 -0
- oscura/session/session.py +54 -46
- 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/utils/autodetect.py +5 -1
- oscura/visualization/digital_advanced.py +718 -0
- oscura/visualization/figure_manager.py +156 -0
- {oscura-0.3.0.dist-info → oscura-0.5.0.dist-info}/METADATA +86 -5
- {oscura-0.3.0.dist-info → oscura-0.5.0.dist-info}/RECORD +54 -33
- oscura/automotive/dtc/data.json +0 -2763
- oscura/schemas/bus_configuration.json +0 -322
- oscura/schemas/device_mapping.json +0 -182
- oscura/schemas/packet_format.json +0 -418
- oscura/schemas/protocol_definition.json +0 -363
- {oscura-0.3.0.dist-info → oscura-0.5.0.dist-info}/WHEEL +0 -0
- {oscura-0.3.0.dist-info → oscura-0.5.0.dist-info}/entry_points.txt +0 -0
- {oscura-0.3.0.dist-info → oscura-0.5.0.dist-info}/licenses/LICENSE +0 -0
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(
|
oscura/builders/__init__.py
CHANGED
|
@@ -7,7 +7,7 @@ generation without manual numpy operations.
|
|
|
7
7
|
Example:
|
|
8
8
|
>>> import oscura as osc
|
|
9
9
|
>>> # Simple sine wave with noise
|
|
10
|
-
>>>
|
|
10
|
+
>>> trace = (osc.SignalBuilder(sample_rate=1e6, duration=0.01)
|
|
11
11
|
... .add_sine(frequency=1000, amplitude=1.0)
|
|
12
12
|
... .add_noise(snr_db=40)
|
|
13
13
|
... .build())
|
|
@@ -19,23 +19,21 @@ Example:
|
|
|
19
19
|
... .build())
|
|
20
20
|
>>>
|
|
21
21
|
>>> # Multi-channel SPI transaction
|
|
22
|
-
>>>
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
>>> builder = osc.SignalBuilder(sample_rate=10e6)
|
|
23
|
+
>>> builder.add_spi(clock_freq=1e6, data_mosi=b"\\x9F\\x00\\x00")
|
|
24
|
+
>>> channels = builder.build_channels() # Returns dict[str, WaveformTrace]
|
|
25
|
+
|
|
26
|
+
API:
|
|
27
|
+
- SignalBuilder.build() returns WaveformTrace for single-channel signals
|
|
28
|
+
- SignalBuilder.build_channels() returns dict[str, WaveformTrace] for multi-channel
|
|
25
29
|
|
|
26
30
|
References:
|
|
27
31
|
- Oscura Signal Generation Guide
|
|
28
32
|
- Protocol Test Signal Specifications
|
|
29
33
|
"""
|
|
30
34
|
|
|
31
|
-
from oscura.builders.signal_builder import
|
|
32
|
-
GeneratedSignal,
|
|
33
|
-
SignalBuilder,
|
|
34
|
-
SignalMetadata,
|
|
35
|
-
)
|
|
35
|
+
from oscura.builders.signal_builder import SignalBuilder
|
|
36
36
|
|
|
37
37
|
__all__ = [
|
|
38
|
-
"GeneratedSignal",
|
|
39
38
|
"SignalBuilder",
|
|
40
|
-
"SignalMetadata",
|
|
41
39
|
]
|