oscura 0.8.0__py3-none-any.whl → 0.11.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 (161) hide show
  1. oscura/__init__.py +19 -19
  2. oscura/__main__.py +4 -0
  3. oscura/analyzers/__init__.py +2 -0
  4. oscura/analyzers/digital/extraction.py +2 -3
  5. oscura/analyzers/digital/quality.py +1 -1
  6. oscura/analyzers/digital/timing.py +1 -1
  7. oscura/analyzers/ml/signal_classifier.py +6 -0
  8. oscura/analyzers/patterns/__init__.py +66 -0
  9. oscura/analyzers/power/basic.py +3 -3
  10. oscura/analyzers/power/soa.py +1 -1
  11. oscura/analyzers/power/switching.py +3 -3
  12. oscura/analyzers/signal_classification.py +529 -0
  13. oscura/analyzers/signal_integrity/sparams.py +3 -3
  14. oscura/analyzers/statistics/basic.py +10 -7
  15. oscura/analyzers/validation.py +1 -1
  16. oscura/analyzers/waveform/measurements.py +200 -156
  17. oscura/analyzers/waveform/measurements_with_uncertainty.py +91 -35
  18. oscura/analyzers/waveform/spectral.py +182 -84
  19. oscura/api/dsl/commands.py +15 -6
  20. oscura/api/server/templates/base.html +137 -146
  21. oscura/api/server/templates/export.html +84 -110
  22. oscura/api/server/templates/home.html +248 -267
  23. oscura/api/server/templates/protocols.html +44 -48
  24. oscura/api/server/templates/reports.html +27 -35
  25. oscura/api/server/templates/session_detail.html +68 -78
  26. oscura/api/server/templates/sessions.html +62 -72
  27. oscura/api/server/templates/waveforms.html +54 -64
  28. oscura/automotive/__init__.py +1 -1
  29. oscura/automotive/can/session.py +1 -1
  30. oscura/automotive/dbc/generator.py +638 -23
  31. oscura/automotive/dtc/data.json +17 -102
  32. oscura/automotive/flexray/fibex.py +9 -1
  33. oscura/automotive/uds/decoder.py +99 -6
  34. oscura/cli/analyze.py +8 -2
  35. oscura/cli/batch.py +36 -5
  36. oscura/cli/characterize.py +18 -4
  37. oscura/cli/export.py +47 -5
  38. oscura/cli/main.py +2 -0
  39. oscura/cli/onboarding/wizard.py +10 -6
  40. oscura/cli/pipeline.py +585 -0
  41. oscura/cli/visualize.py +6 -4
  42. oscura/convenience.py +400 -32
  43. oscura/core/measurement_result.py +286 -0
  44. oscura/core/progress.py +1 -1
  45. oscura/core/schemas/device_mapping.json +2 -8
  46. oscura/core/schemas/packet_format.json +4 -24
  47. oscura/core/schemas/protocol_definition.json +2 -12
  48. oscura/core/types.py +232 -239
  49. oscura/correlation/multi_protocol.py +1 -1
  50. oscura/export/legacy/__init__.py +11 -0
  51. oscura/export/legacy/wav.py +75 -0
  52. oscura/exporters/__init__.py +19 -0
  53. oscura/exporters/wireshark.py +809 -0
  54. oscura/hardware/acquisition/file.py +5 -19
  55. oscura/hardware/acquisition/saleae.py +10 -10
  56. oscura/hardware/acquisition/socketcan.py +4 -6
  57. oscura/hardware/acquisition/synthetic.py +1 -5
  58. oscura/hardware/acquisition/visa.py +6 -6
  59. oscura/hardware/security/side_channel_detector.py +5 -508
  60. oscura/inference/message_format.py +686 -1
  61. oscura/jupyter/display.py +2 -2
  62. oscura/jupyter/magic.py +3 -3
  63. oscura/loaders/__init__.py +17 -12
  64. oscura/loaders/binary.py +1 -1
  65. oscura/loaders/chipwhisperer.py +1 -2
  66. oscura/loaders/configurable.py +1 -1
  67. oscura/loaders/csv_loader.py +2 -2
  68. oscura/loaders/hdf5_loader.py +1 -1
  69. oscura/loaders/lazy.py +6 -1
  70. oscura/loaders/mmap_loader.py +0 -1
  71. oscura/loaders/numpy_loader.py +8 -7
  72. oscura/loaders/preprocessing.py +3 -5
  73. oscura/loaders/rigol.py +21 -7
  74. oscura/loaders/sigrok.py +2 -5
  75. oscura/loaders/tdms.py +3 -2
  76. oscura/loaders/tektronix.py +38 -32
  77. oscura/loaders/tss.py +20 -27
  78. oscura/loaders/validation.py +17 -10
  79. oscura/loaders/vcd.py +13 -8
  80. oscura/loaders/wav.py +1 -6
  81. oscura/pipeline/__init__.py +76 -0
  82. oscura/pipeline/handlers/__init__.py +165 -0
  83. oscura/pipeline/handlers/analyzers.py +1045 -0
  84. oscura/pipeline/handlers/decoders.py +899 -0
  85. oscura/pipeline/handlers/exporters.py +1103 -0
  86. oscura/pipeline/handlers/filters.py +891 -0
  87. oscura/pipeline/handlers/loaders.py +640 -0
  88. oscura/pipeline/handlers/transforms.py +768 -0
  89. oscura/reporting/formatting/measurements.py +55 -14
  90. oscura/reporting/templates/enhanced/protocol_re.html +504 -503
  91. oscura/sessions/legacy.py +49 -1
  92. oscura/side_channel/__init__.py +38 -57
  93. oscura/utils/builders/signal_builder.py +5 -5
  94. oscura/utils/comparison/compare.py +7 -9
  95. oscura/utils/comparison/golden.py +1 -1
  96. oscura/utils/filtering/convenience.py +2 -2
  97. oscura/utils/math/arithmetic.py +38 -62
  98. oscura/utils/math/interpolation.py +20 -20
  99. oscura/utils/pipeline/__init__.py +4 -17
  100. oscura/utils/progressive.py +1 -4
  101. oscura/utils/triggering/edge.py +1 -1
  102. oscura/utils/triggering/pattern.py +2 -2
  103. oscura/utils/triggering/pulse.py +2 -2
  104. oscura/utils/triggering/window.py +3 -3
  105. oscura/validation/hil_testing.py +11 -11
  106. oscura/visualization/__init__.py +46 -284
  107. oscura/visualization/batch.py +72 -433
  108. oscura/visualization/plot.py +542 -53
  109. oscura/visualization/styles.py +184 -318
  110. oscura/workflows/batch/advanced.py +1 -1
  111. oscura/workflows/batch/aggregate.py +12 -9
  112. oscura/workflows/complete_re.py +251 -23
  113. oscura/workflows/digital.py +27 -4
  114. oscura/workflows/multi_trace.py +136 -17
  115. oscura/workflows/waveform.py +11 -6
  116. oscura-0.11.0.dist-info/METADATA +460 -0
  117. {oscura-0.8.0.dist-info → oscura-0.11.0.dist-info}/RECORD +120 -145
  118. oscura/side_channel/dpa.py +0 -1025
  119. oscura/utils/optimization/__init__.py +0 -19
  120. oscura/utils/optimization/parallel.py +0 -443
  121. oscura/utils/optimization/search.py +0 -532
  122. oscura/utils/pipeline/base.py +0 -338
  123. oscura/utils/pipeline/composition.py +0 -248
  124. oscura/utils/pipeline/parallel.py +0 -449
  125. oscura/utils/pipeline/pipeline.py +0 -375
  126. oscura/utils/search/__init__.py +0 -16
  127. oscura/utils/search/anomaly.py +0 -424
  128. oscura/utils/search/context.py +0 -294
  129. oscura/utils/search/pattern.py +0 -288
  130. oscura/utils/storage/__init__.py +0 -61
  131. oscura/utils/storage/database.py +0 -1166
  132. oscura/visualization/accessibility.py +0 -526
  133. oscura/visualization/annotations.py +0 -371
  134. oscura/visualization/axis_scaling.py +0 -305
  135. oscura/visualization/colors.py +0 -451
  136. oscura/visualization/digital.py +0 -436
  137. oscura/visualization/eye.py +0 -571
  138. oscura/visualization/histogram.py +0 -281
  139. oscura/visualization/interactive.py +0 -1035
  140. oscura/visualization/jitter.py +0 -1042
  141. oscura/visualization/keyboard.py +0 -394
  142. oscura/visualization/layout.py +0 -400
  143. oscura/visualization/optimization.py +0 -1079
  144. oscura/visualization/palettes.py +0 -446
  145. oscura/visualization/power.py +0 -508
  146. oscura/visualization/power_extended.py +0 -955
  147. oscura/visualization/presets.py +0 -469
  148. oscura/visualization/protocols.py +0 -1246
  149. oscura/visualization/render.py +0 -223
  150. oscura/visualization/rendering.py +0 -444
  151. oscura/visualization/reverse_engineering.py +0 -838
  152. oscura/visualization/signal_integrity.py +0 -989
  153. oscura/visualization/specialized.py +0 -643
  154. oscura/visualization/spectral.py +0 -1226
  155. oscura/visualization/thumbnails.py +0 -340
  156. oscura/visualization/time_axis.py +0 -351
  157. oscura/visualization/waveform.py +0 -454
  158. oscura-0.8.0.dist-info/METADATA +0 -661
  159. {oscura-0.8.0.dist-info → oscura-0.11.0.dist-info}/WHEEL +0 -0
  160. {oscura-0.8.0.dist-info → oscura-0.11.0.dist-info}/entry_points.txt +0 -0
  161. {oscura-0.8.0.dist-info → oscura-0.11.0.dist-info}/licenses/LICENSE +0 -0
@@ -266,12 +266,7 @@
266
266
  "category": "Powertrain",
267
267
  "severity": "High",
268
268
  "system": "Throttle Control",
269
- "possible_causes": [
270
- "TPS circuit shorted to voltage",
271
- "Faulty TPS sensor",
272
- "Wiring harness open",
273
- "ECM problem"
274
- ]
269
+ "possible_causes": ["TPS circuit shorted to voltage", "Faulty TPS sensor", "Wiring harness open", "ECM problem"]
275
270
  },
276
271
  "P0125": {
277
272
  "code": "P0125",
@@ -865,12 +860,7 @@
865
860
  "category": "Powertrain",
866
861
  "severity": "Medium",
867
862
  "system": "Emissions Control",
868
- "possible_causes": [
869
- "EGR valve stuck closed",
870
- "EGR passages clogged",
871
- "Faulty EGR valve",
872
- "Vacuum leak"
873
- ]
863
+ "possible_causes": ["EGR valve stuck closed", "EGR passages clogged", "Faulty EGR valve", "Vacuum leak"]
874
864
  },
875
865
  "P0401": {
876
866
  "code": "P0401",
@@ -891,12 +881,7 @@
891
881
  "category": "Powertrain",
892
882
  "severity": "Medium",
893
883
  "system": "Emissions Control",
894
- "possible_causes": [
895
- "EGR valve stuck open",
896
- "Faulty EGR valve",
897
- "EGR vacuum solenoid fault",
898
- "ECM problem"
899
- ]
884
+ "possible_causes": ["EGR valve stuck open", "Faulty EGR valve", "EGR vacuum solenoid fault", "ECM problem"]
900
885
  },
901
886
  "P0403": {
902
887
  "code": "P0403",
@@ -958,12 +943,7 @@
958
943
  "category": "Powertrain",
959
944
  "severity": "Low",
960
945
  "system": "Emissions Control",
961
- "possible_causes": [
962
- "Loose or missing fuel cap",
963
- "EVAP system leak",
964
- "Faulty purge valve",
965
- "Faulty vent valve"
966
- ]
946
+ "possible_causes": ["Loose or missing fuel cap", "EVAP system leak", "Faulty purge valve", "Faulty vent valve"]
967
947
  },
968
948
  "P0441": {
969
949
  "code": "P0441",
@@ -1075,12 +1055,7 @@
1075
1055
  "category": "Powertrain",
1076
1056
  "severity": "Low",
1077
1057
  "system": "Idle Control",
1078
- "possible_causes": [
1079
- "Vacuum leak",
1080
- "IAC valve fault",
1081
- "Dirty throttle body",
1082
- "PCV valve problem"
1083
- ]
1058
+ "possible_causes": ["Vacuum leak", "IAC valve fault", "Dirty throttle body", "PCV valve problem"]
1084
1059
  },
1085
1060
  "P0507": {
1086
1061
  "code": "P0507",
@@ -1088,12 +1063,7 @@
1088
1063
  "category": "Powertrain",
1089
1064
  "severity": "Low",
1090
1065
  "system": "Idle Control",
1091
- "possible_causes": [
1092
- "Vacuum leak",
1093
- "IAC valve stuck open",
1094
- "PCV valve stuck open",
1095
- "EVAP purge valve leaking"
1096
- ]
1066
+ "possible_causes": ["Vacuum leak", "IAC valve stuck open", "PCV valve stuck open", "EVAP purge valve leaking"]
1097
1067
  },
1098
1068
  "P0600": {
1099
1069
  "code": "P0600",
@@ -1127,12 +1097,7 @@
1127
1097
  "category": "Powertrain",
1128
1098
  "severity": "Critical",
1129
1099
  "system": "Engine Control Module",
1130
- "possible_causes": [
1131
- "ECM not programmed",
1132
- "ECM programming incomplete",
1133
- "Wrong software version",
1134
- "ECM fault"
1135
- ]
1100
+ "possible_causes": ["ECM not programmed", "ECM programming incomplete", "Wrong software version", "ECM fault"]
1136
1101
  },
1137
1102
  "P0603": {
1138
1103
  "code": "P0603",
@@ -1205,12 +1170,7 @@
1205
1170
  "category": "Powertrain",
1206
1171
  "severity": "Medium",
1207
1172
  "system": "Charging System",
1208
- "possible_causes": [
1209
- "Faulty alternator",
1210
- "Wiring harness problem",
1211
- "Poor electrical connection",
1212
- "ECM fault"
1213
- ]
1173
+ "possible_causes": ["Faulty alternator", "Wiring harness problem", "Poor electrical connection", "ECM fault"]
1214
1174
  },
1215
1175
  "P0625": {
1216
1176
  "code": "P0625",
@@ -1283,12 +1243,7 @@
1283
1243
  "category": "Powertrain",
1284
1244
  "severity": "High",
1285
1245
  "system": "Transmission",
1286
- "possible_causes": [
1287
- "Faulty input speed sensor",
1288
- "Wiring harness problem",
1289
- "Sensor reluctor damaged",
1290
- "TCM fault"
1291
- ]
1246
+ "possible_causes": ["Faulty input speed sensor", "Wiring harness problem", "Sensor reluctor damaged", "TCM fault"]
1292
1247
  },
1293
1248
  "P0720": {
1294
1249
  "code": "P0720",
@@ -1491,12 +1446,7 @@
1491
1446
  "category": "Chassis",
1492
1447
  "severity": "High",
1493
1448
  "system": "ABS",
1494
- "possible_causes": [
1495
- "Faulty valve relay",
1496
- "Relay circuit problem",
1497
- "ABS module fault",
1498
- "Wiring harness issue"
1499
- ]
1449
+ "possible_causes": ["Faulty valve relay", "Relay circuit problem", "ABS module fault", "Wiring harness issue"]
1500
1450
  },
1501
1451
  "C0161": {
1502
1452
  "code": "C0161",
@@ -2206,12 +2156,7 @@
2206
2156
  "category": "Body",
2207
2157
  "severity": "Low",
2208
2158
  "system": "Lighting System",
2209
- "possible_causes": [
2210
- "Burned out bulb",
2211
- "Wiring harness problem",
2212
- "Lamp socket corrosion",
2213
- "BCM fault"
2214
- ]
2159
+ "possible_causes": ["Burned out bulb", "Wiring harness problem", "Lamp socket corrosion", "BCM fault"]
2215
2160
  },
2216
2161
  "B0601": {
2217
2162
  "code": "B0601",
@@ -2232,12 +2177,7 @@
2232
2177
  "category": "Body",
2233
2178
  "severity": "Low",
2234
2179
  "system": "Lighting System",
2235
- "possible_causes": [
2236
- "Burned out turn signal bulb",
2237
- "Wiring harness problem",
2238
- "Flasher relay fault",
2239
- "BCM fault"
2240
- ]
2180
+ "possible_causes": ["Burned out turn signal bulb", "Wiring harness problem", "Flasher relay fault", "BCM fault"]
2241
2181
  },
2242
2182
  "B0603": {
2243
2183
  "code": "B0603",
@@ -2245,12 +2185,7 @@
2245
2185
  "category": "Body",
2246
2186
  "severity": "Low",
2247
2187
  "system": "Lighting System",
2248
- "possible_causes": [
2249
- "Burned out turn signal bulb",
2250
- "Wiring harness problem",
2251
- "Flasher relay fault",
2252
- "BCM fault"
2253
- ]
2188
+ "possible_causes": ["Burned out turn signal bulb", "Wiring harness problem", "Flasher relay fault", "BCM fault"]
2254
2189
  },
2255
2190
  "B0604": {
2256
2191
  "code": "B0604",
@@ -2362,12 +2297,7 @@
2362
2297
  "category": "Body",
2363
2298
  "severity": "Low",
2364
2299
  "system": "Keyless Entry",
2365
- "possible_causes": [
2366
- "Key fob battery weak",
2367
- "Key fob not synchronized",
2368
- "BCM fault",
2369
- "Receiver antenna fault"
2370
- ]
2300
+ "possible_causes": ["Key fob battery weak", "Key fob not synchronized", "BCM fault", "Receiver antenna fault"]
2371
2301
  },
2372
2302
  "B1300": {
2373
2303
  "code": "B1300",
@@ -2466,12 +2396,7 @@
2466
2396
  "category": "Network",
2467
2397
  "severity": "Critical",
2468
2398
  "system": "CAN Bus",
2469
- "possible_causes": [
2470
- "TCM not powered",
2471
- "CAN bus wiring problem",
2472
- "TCM internal fault",
2473
- "CAN bus short circuit"
2474
- ]
2399
+ "possible_causes": ["TCM not powered", "CAN bus wiring problem", "TCM internal fault", "CAN bus short circuit"]
2475
2400
  },
2476
2401
  "U0102": {
2477
2402
  "code": "U0102",
@@ -2544,12 +2469,7 @@
2544
2469
  "category": "Network",
2545
2470
  "severity": "High",
2546
2471
  "system": "CAN Bus",
2547
- "possible_causes": [
2548
- "BCM not powered",
2549
- "CAN bus wiring problem",
2550
- "BCM internal fault",
2551
- "Ground connection issue"
2552
- ]
2472
+ "possible_causes": ["BCM not powered", "CAN bus wiring problem", "BCM internal fault", "Ground connection issue"]
2553
2473
  },
2554
2474
  "U0141": {
2555
2475
  "code": "U0141",
@@ -2557,12 +2477,7 @@
2557
2477
  "category": "Network",
2558
2478
  "severity": "High",
2559
2479
  "system": "CAN Bus",
2560
- "possible_causes": [
2561
- "BCM not powered",
2562
- "CAN bus wiring problem",
2563
- "Module internal fault",
2564
- "Connector problem"
2565
- ]
2480
+ "possible_causes": ["BCM not powered", "CAN bus wiring problem", "Module internal fault", "Connector problem"]
2566
2481
  },
2567
2482
  "U0151": {
2568
2483
  "code": "U0151",
@@ -299,7 +299,15 @@ class FIBEXImporter:
299
299
  if not fibex_path.exists():
300
300
  raise FileNotFoundError(f"FIBEX file not found: {fibex_path}")
301
301
 
302
- tree = ET.parse(fibex_path)
302
+ # SEC-004: Protect against XXE attacks by disabling entity expansion
303
+ parser = ET.XMLParser()
304
+ try:
305
+ # Python < 3.12: entity attribute is writable
306
+ parser.entity = {} # type: ignore[misc]
307
+ except AttributeError:
308
+ # Python >= 3.12: entity attribute is read-only, default behavior is safe
309
+ pass
310
+ tree = ET.parse(fibex_path, parser=parser)
303
311
  root = tree.getroot()
304
312
 
305
313
  # Extract cluster configuration
@@ -5,6 +5,7 @@ This module implements decoding for UDS diagnostic messages used in automotive E
5
5
 
6
6
  from __future__ import annotations
7
7
 
8
+ from dataclasses import dataclass
8
9
  from typing import TYPE_CHECKING
9
10
 
10
11
  if TYPE_CHECKING:
@@ -12,7 +13,26 @@ if TYPE_CHECKING:
12
13
 
13
14
  from oscura.automotive.uds.models import UDSNegativeResponse, UDSService
14
15
 
15
- __all__ = ["UDSDecoder"]
16
+ __all__ = ["UDSDecoder", "UDSResponse"]
17
+
18
+
19
+ @dataclass
20
+ class UDSResponse:
21
+ """A decoded UDS response message.
22
+
23
+ Attributes:
24
+ service: Service ID.
25
+ data: Response data payload.
26
+ timestamp: Message timestamp.
27
+ is_negative: True if negative response (NRC).
28
+ nrc: Negative Response Code (if is_negative=True).
29
+ """
30
+
31
+ service: int
32
+ data: bytes
33
+ timestamp: float
34
+ is_negative: bool = False
35
+ nrc: int | None = None
16
36
 
17
37
 
18
38
  # Service ID mappings per ISO 14229-1
@@ -146,6 +166,58 @@ class UDSDecoder:
146
166
 
147
167
  return False
148
168
 
169
+ @staticmethod
170
+ def decode(message: CANMessage) -> UDSResponse | None:
171
+ """Decode UDS message from CAN message (returns UDSResponse).
172
+
173
+ Args:
174
+ message: CAN message to decode.
175
+
176
+ Returns:
177
+ UDSResponse with decoded information, or None if not a valid UDS message.
178
+
179
+ Example:
180
+ >>> msg = CANMessage(id=0x7E0, timestamp=1.0, data=bytes([0x02, 0x10, 0x01]))
181
+ >>> response = UDSDecoder.decode(msg)
182
+ >>> print(f"Service: 0x{response.service:02X}")
183
+ """
184
+ if len(message.data) < 2:
185
+ return None
186
+
187
+ # Extract UDS payload from ISO-TP frame if needed
188
+ data = UDSDecoder._extract_uds_payload(message.data)
189
+ if not data:
190
+ return None
191
+
192
+ # Check for negative response
193
+ if data[0] == 0x7F:
194
+ neg_resp = UDSDecoder._decode_negative_response(data)
195
+ if neg_resp is None:
196
+ return None
197
+ return UDSResponse(
198
+ service=0x7F,
199
+ data=data,
200
+ timestamp=message.timestamp,
201
+ is_negative=True,
202
+ nrc=neg_resp.nrc,
203
+ )
204
+
205
+ # Determine SID and request/response type
206
+ sid_info = UDSDecoder._parse_sid_byte(data[0])
207
+ if sid_info is None:
208
+ return None
209
+
210
+ sid, canonical_sid, is_request = sid_info
211
+
212
+ # Return UDSResponse
213
+ return UDSResponse(
214
+ service=sid,
215
+ data=data,
216
+ timestamp=message.timestamp,
217
+ is_negative=False,
218
+ nrc=None,
219
+ )
220
+
149
221
  @staticmethod
150
222
  def decode_service(message: CANMessage) -> UDSService | UDSNegativeResponse | None:
151
223
  """Decode UDS service from CAN message.
@@ -198,7 +270,11 @@ class UDSDecoder:
198
270
  def _extract_uds_payload(message_data: bytes) -> bytes:
199
271
  """Extract UDS payload from CAN message data.
200
272
 
201
- Handles ISO-TP single frame format (first byte ≤0x07 indicates length).
273
+ Handles ISO-TP frame formats:
274
+ - Single frame: 0x0X (X = length)
275
+ - First frame: 0x1X (X = upper nibble of length)
276
+ - Consecutive frame: 0x2X (X = sequence number)
277
+ - Flow control: 0x3X
202
278
 
203
279
  Args:
204
280
  message_data: Raw CAN message data.
@@ -206,14 +282,31 @@ class UDSDecoder:
206
282
  Returns:
207
283
  UDS payload bytes (empty if invalid).
208
284
  """
209
- if message_data[0] <= 0x07:
210
- # ISO-TP single frame: [length, ...UDS data...]
211
- uds_length = message_data[0]
285
+ pci = message_data[0]
286
+ pci_type = (pci >> 4) & 0x0F
287
+
288
+ if pci_type == 0:
289
+ # ISO-TP single frame: [0x0X, ...UDS data...]
290
+ uds_length = pci & 0x0F
291
+ if len(message_data) < 1 + uds_length:
292
+ return b""
293
+ return message_data[1 : 1 + uds_length]
294
+ elif pci_type == 1:
295
+ # ISO-TP first frame: [0x1X, length_low, ...UDS data...]
296
+ # Skip PCI bytes (2 bytes) and return remaining as UDS payload
297
+ return message_data[2:]
298
+ elif pci_type == 2:
299
+ # ISO-TP consecutive frame: [0x2X, ...data...]
300
+ # Skip PCI byte and return data (but this is continuation, not standalone UDS)
301
+ return message_data[1:]
302
+ elif pci <= 0x07:
303
+ # Legacy single frame format (for compatibility)
304
+ uds_length = pci
212
305
  if len(message_data) < 1 + uds_length:
213
306
  return b""
214
307
  return message_data[1 : 1 + uds_length]
215
308
  else:
216
- # Direct UDS: all bytes are UDS data
309
+ # Direct UDS or unknown format: treat all bytes as UDS data
217
310
  return message_data
218
311
 
219
312
  @staticmethod
oscura/cli/analyze.py CHANGED
@@ -222,14 +222,20 @@ def _characterize_signal(trace: Any) -> dict[str, Any]:
222
222
 
223
223
  rt = rise_time(trace)
224
224
  ft = fall_time(trace)
225
+ rt_val = rt["value"] if rt["applicable"] else None
226
+ ft_val = ft["value"] if ft["applicable"] else None
225
227
 
226
228
  return {
227
229
  "sample_rate": f"{sample_rate / 1e6:.1f} MHz",
228
230
  "samples": len(data),
229
231
  "duration": f"{len(data) / sample_rate * 1e3:.3f} ms",
230
232
  "amplitude": f"{float(data.max() - data.min()):.3f} V",
231
- "rise_time": f"{rt * 1e9:.2f} ns" if not np.isnan(rt) else "N/A",
232
- "fall_time": f"{ft * 1e9:.2f} ns" if not np.isnan(ft) else "N/A",
233
+ "rise_time": f"{rt_val * 1e9:.2f} ns"
234
+ if rt_val is not None and not np.isnan(rt_val)
235
+ else "N/A",
236
+ "fall_time": f"{ft_val * 1e9:.2f} ns"
237
+ if ft_val is not None and not np.isnan(ft_val)
238
+ else "N/A",
233
239
  }
234
240
 
235
241
 
oscura/cli/batch.py CHANGED
@@ -169,17 +169,37 @@ def _analyze_single_file(file_path: str, analysis_type: str) -> dict[str, Any]:
169
169
  "file": str(Path(file_path).name),
170
170
  "status": "success",
171
171
  "analysis_type": analysis_type,
172
- "samples": len(trace.data), # type: ignore[union-attr]
172
+ "samples": len(trace.data),
173
173
  "sample_rate": f"{sample_rate / 1e6:.1f} MHz",
174
174
  }
175
175
 
176
176
  if analysis_type == "characterize":
177
177
  rt = rise_time(trace) # type: ignore[arg-type]
178
178
  ft = fall_time(trace) # type: ignore[arg-type]
179
+
180
+ # Handle both MeasurementResult dict and raw float/nan values (for test compatibility)
181
+ if isinstance(rt, dict):
182
+ rt_val = rt["value"] if rt.get("applicable", True) else None
183
+ elif isinstance(rt, (int, float)): # type: ignore[unreachable]
184
+ rt_val = rt
185
+ else:
186
+ rt_val = None
187
+
188
+ if isinstance(ft, dict):
189
+ ft_val = ft["value"] if ft.get("applicable", True) else None
190
+ elif isinstance(ft, (int, float)): # type: ignore[unreachable]
191
+ ft_val = ft
192
+ else:
193
+ ft_val = None
194
+
179
195
  result.update(
180
196
  {
181
- "rise_time": f"{rt * 1e9:.2f} ns" if not np.isnan(rt) else "N/A",
182
- "fall_time": f"{ft * 1e9:.2f} ns" if not np.isnan(ft) else "N/A",
197
+ "rise_time": f"{rt_val * 1e9:.2f} ns"
198
+ if rt_val is not None and not np.isnan(rt_val)
199
+ else "N/A",
200
+ "fall_time": f"{ft_val * 1e9:.2f} ns"
201
+ if ft_val is not None and not np.isnan(ft_val)
202
+ else "N/A",
183
203
  }
184
204
  )
185
205
  elif analysis_type == "decode":
@@ -197,11 +217,22 @@ def _analyze_single_file(file_path: str, analysis_type: str) -> dict[str, Any]:
197
217
  peak_freq = freqs[peak_idx]
198
218
  else:
199
219
  peak_freq = 0.0
200
- thd_val = thd(trace) # type: ignore[arg-type]
220
+ thd_result = thd(trace) # type: ignore[arg-type]
221
+
222
+ # Handle both MeasurementResult dict and raw float/nan values (for test compatibility)
223
+ if isinstance(thd_result, dict):
224
+ thd_val = thd_result["value"] if thd_result.get("applicable", True) else None
225
+ elif isinstance(thd_result, (int, float)): # type: ignore[unreachable]
226
+ thd_val = thd_result
227
+ else:
228
+ thd_val = None
229
+
201
230
  result.update(
202
231
  {
203
232
  "peak_frequency": f"{peak_freq / 1e6:.3f} MHz",
204
- "thd": f"{thd_val:.1f} dB" if not np.isnan(thd_val) else "N/A",
233
+ "thd": f"{thd_val:.1f} dB"
234
+ if thd_val is not None and not np.isnan(thd_val)
235
+ else "N/A",
205
236
  }
206
237
  )
207
238
 
@@ -243,12 +243,26 @@ def _add_buffer_results(results: dict[str, Any], trace: Any, logic_family: str)
243
243
  os_pct = overshoot(trace)
244
244
  us_pct = undershoot(trace)
245
245
 
246
+ # Extract values from MeasurementResult dicts
247
+ rt_val = rt["value"] if isinstance(rt, dict) and rt.get("applicable") else None
248
+ ft_val = ft["value"] if isinstance(ft, dict) and ft.get("applicable") else None
249
+ os_val = os_pct["value"] if isinstance(os_pct, dict) and os_pct.get("applicable") else None
250
+ us_val = us_pct["value"] if isinstance(us_pct, dict) and us_pct.get("applicable") else None
251
+
246
252
  results.update(
247
253
  {
248
- "rise_time": f"{rt * 1e9:.2f} ns" if not np.isnan(rt) else "N/A",
249
- "fall_time": f"{ft * 1e9:.2f} ns" if not np.isnan(ft) else "N/A",
250
- "overshoot": f"{os_pct:.1f} %" if not np.isnan(os_pct) else "N/A",
251
- "undershoot": f"{us_pct:.1f} %" if not np.isnan(us_pct) else "N/A",
254
+ "rise_time": f"{rt_val * 1e9:.2f} ns"
255
+ if rt_val is not None and not np.isnan(rt_val)
256
+ else "N/A",
257
+ "fall_time": f"{ft_val * 1e9:.2f} ns"
258
+ if ft_val is not None and not np.isnan(ft_val)
259
+ else "N/A",
260
+ "overshoot": f"{os_val:.1f} %"
261
+ if os_val is not None and not np.isnan(os_val)
262
+ else "N/A",
263
+ "undershoot": f"{us_val:.1f} %"
264
+ if us_val is not None and not np.isnan(us_val)
265
+ else "N/A",
252
266
  "status": "PASS",
253
267
  }
254
268
  )
oscura/cli/export.py CHANGED
@@ -195,14 +195,56 @@ def _export_csv(session: Any, output_path: Path) -> None:
195
195
 
196
196
 
197
197
  def _export_matlab(session: Any, output_path: Path) -> None:
198
- """Export to MATLAB format.
198
+ """Export to MATLAB .mat format.
199
+
200
+ Exports session data and traces to MATLAB-compatible .mat file using scipy.
199
201
 
200
202
  Args:
201
- session: Session object.
202
- output_path: Output file path.
203
+ session: Session object with traces attribute.
204
+ output_path: Output file path (.mat extension).
205
+
206
+ Raises:
207
+ ImportError: If scipy is not installed.
203
208
  """
204
- # MATLAB export has been redesigned - use new AnalysisSession API
205
- raise NotImplementedError("MATLAB export needs reimplementation with new API")
209
+ try:
210
+ from scipy.io import savemat
211
+ except ImportError as e:
212
+ raise ImportError(
213
+ "scipy is required for MATLAB export. Install with: pip install scipy"
214
+ ) from e
215
+
216
+ import numpy as np
217
+
218
+ # Collect data to export
219
+ matlab_data: dict[str, Any] = {}
220
+
221
+ # Export traces if available
222
+ if hasattr(session, "traces") and session.traces:
223
+ for i, trace in enumerate(session.traces):
224
+ trace_name = f"trace_{i + 1}"
225
+ if hasattr(trace, "data"):
226
+ matlab_data[f"{trace_name}_data"] = np.asarray(trace.data)
227
+ if hasattr(trace, "metadata") and hasattr(trace.metadata, "sample_rate"):
228
+ matlab_data[f"{trace_name}_sample_rate"] = trace.metadata.sample_rate
229
+
230
+ # Export results if available
231
+ if hasattr(session, "results") and session.results:
232
+ if isinstance(session.results, dict):
233
+ for key, value in session.results.items():
234
+ # Convert to numpy-compatible format
235
+ if isinstance(value, (int, float, bool)):
236
+ matlab_data[key] = value
237
+ elif isinstance(value, (list, tuple)):
238
+ matlab_data[key] = np.array(value)
239
+ elif isinstance(value, np.ndarray):
240
+ matlab_data[key] = value
241
+
242
+ if not matlab_data:
243
+ matlab_data["message"] = "No data available for export"
244
+
245
+ # Save to .mat file
246
+ savemat(str(output_path), matlab_data)
247
+ click.echo(f"✓ Exported to MATLAB: {output_path}")
206
248
 
207
249
 
208
250
  def _export_wireshark(session: Any, output_path: Path) -> None:
oscura/cli/main.py CHANGED
@@ -248,6 +248,7 @@ from oscura.cli.compare import compare
248
248
  from oscura.cli.config_cmd import config as config_cmd
249
249
  from oscura.cli.decode import decode
250
250
  from oscura.cli.export import export
251
+ from oscura.cli.pipeline import pipeline
251
252
  from oscura.cli.validate_cmd import validate
252
253
  from oscura.cli.visualize import visualize
253
254
 
@@ -312,6 +313,7 @@ cli.add_command(config_cmd, name="config")
312
313
  cli.add_command(characterize) # type: ignore[has-type]
313
314
  cli.add_command(batch) # type: ignore[has-type]
314
315
  cli.add_command(compare) # type: ignore[has-type]
316
+ cli.add_command(pipeline) # type: ignore[has-type]
315
317
  cli.add_command(shell)
316
318
  cli.add_command(tutorial)
317
319
 
@@ -224,8 +224,8 @@ class AnalysisWizard:
224
224
  else:
225
225
  print(f" Sample rate: {rate / 1e3:.3f} kSa/s")
226
226
 
227
- if hasattr(meta, "channel_name") and meta.channel_name:
228
- print(f" Channel: {meta.channel_name}")
227
+ if hasattr(meta, "channel") and meta.channel:
228
+ print(f" Channel: {meta.channel}")
229
229
 
230
230
  if hasattr(trace, "data"):
231
231
  import numpy as np
@@ -375,13 +375,17 @@ class AnalysisWizard:
375
375
  print(f" SNR: {snr_val:.1f} dB")
376
376
 
377
377
  # Recommendations based on quality
378
- if thd_val > -40:
378
+ # Extract values from MeasurementResult dicts
379
+ thd_value = thd_val["value"] if isinstance(thd_val, dict) else thd_val
380
+ snr_value = snr_val["value"] if isinstance(snr_val, dict) else snr_val
381
+
382
+ if thd_value is not None and thd_value > -40:
379
383
  self.result.recommendations.append(
380
- f"THD is {thd_val:.1f} dB - consider filtering to reduce distortion"
384
+ f"THD is {thd_value:.1f} dB - consider filtering to reduce distortion"
381
385
  )
382
- if snr_val < 40:
386
+ if snr_value is not None and snr_value < 40:
383
387
  self.result.recommendations.append(
384
- f"SNR is {snr_val:.1f} dB - signal is noisy, try averaging or filtering"
388
+ f"SNR is {snr_value:.1f} dB - signal is noisy, try averaging or filtering"
385
389
  )
386
390
 
387
391
  if choice in (2, 3): # Anomalies