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
@@ -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(
@@ -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
- >>> signal = (osc.SignalBuilder(sample_rate=1e6, duration=0.01)
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
- >>> spi = (osc.SignalBuilder(sample_rate=10e6)
23
- ... .add_spi(clock_freq=1e6, data_mosi=b"\\x9F\\x00\\x00")
24
- ... .build())
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
  ]