oscura 0.7.0__py3-none-any.whl → 0.10.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 (175) hide show
  1. oscura/__init__.py +19 -19
  2. oscura/analyzers/__init__.py +2 -0
  3. oscura/analyzers/digital/extraction.py +2 -3
  4. oscura/analyzers/digital/quality.py +1 -1
  5. oscura/analyzers/digital/timing.py +1 -1
  6. oscura/analyzers/eye/__init__.py +5 -1
  7. oscura/analyzers/eye/generation.py +501 -0
  8. oscura/analyzers/jitter/__init__.py +6 -6
  9. oscura/analyzers/jitter/timing.py +419 -0
  10. oscura/analyzers/patterns/__init__.py +94 -0
  11. oscura/analyzers/patterns/reverse_engineering.py +991 -0
  12. oscura/analyzers/power/__init__.py +35 -12
  13. oscura/analyzers/power/basic.py +3 -3
  14. oscura/analyzers/power/soa.py +1 -1
  15. oscura/analyzers/power/switching.py +3 -3
  16. oscura/analyzers/signal_classification.py +529 -0
  17. oscura/analyzers/signal_integrity/sparams.py +3 -3
  18. oscura/analyzers/statistics/__init__.py +4 -0
  19. oscura/analyzers/statistics/basic.py +152 -0
  20. oscura/analyzers/statistics/correlation.py +47 -6
  21. oscura/analyzers/validation.py +1 -1
  22. oscura/analyzers/waveform/__init__.py +2 -0
  23. oscura/analyzers/waveform/measurements.py +329 -163
  24. oscura/analyzers/waveform/measurements_with_uncertainty.py +91 -35
  25. oscura/analyzers/waveform/spectral.py +498 -54
  26. oscura/api/dsl/commands.py +15 -6
  27. oscura/api/server/templates/base.html +137 -146
  28. oscura/api/server/templates/export.html +84 -110
  29. oscura/api/server/templates/home.html +248 -267
  30. oscura/api/server/templates/protocols.html +44 -48
  31. oscura/api/server/templates/reports.html +27 -35
  32. oscura/api/server/templates/session_detail.html +68 -78
  33. oscura/api/server/templates/sessions.html +62 -72
  34. oscura/api/server/templates/waveforms.html +54 -64
  35. oscura/automotive/__init__.py +1 -1
  36. oscura/automotive/can/session.py +1 -1
  37. oscura/automotive/dbc/generator.py +638 -23
  38. oscura/automotive/dtc/data.json +102 -17
  39. oscura/automotive/uds/decoder.py +99 -6
  40. oscura/cli/analyze.py +8 -2
  41. oscura/cli/batch.py +36 -5
  42. oscura/cli/characterize.py +18 -4
  43. oscura/cli/export.py +47 -5
  44. oscura/cli/main.py +2 -0
  45. oscura/cli/onboarding/wizard.py +10 -6
  46. oscura/cli/pipeline.py +585 -0
  47. oscura/cli/visualize.py +6 -4
  48. oscura/convenience.py +400 -32
  49. oscura/core/config/loader.py +0 -1
  50. oscura/core/measurement_result.py +286 -0
  51. oscura/core/progress.py +1 -1
  52. oscura/core/schemas/device_mapping.json +8 -2
  53. oscura/core/schemas/packet_format.json +24 -4
  54. oscura/core/schemas/protocol_definition.json +12 -2
  55. oscura/core/types.py +300 -199
  56. oscura/correlation/multi_protocol.py +1 -1
  57. oscura/export/legacy/__init__.py +11 -0
  58. oscura/export/legacy/wav.py +75 -0
  59. oscura/exporters/__init__.py +19 -0
  60. oscura/exporters/wireshark.py +809 -0
  61. oscura/hardware/acquisition/file.py +5 -19
  62. oscura/hardware/acquisition/saleae.py +10 -10
  63. oscura/hardware/acquisition/socketcan.py +4 -6
  64. oscura/hardware/acquisition/synthetic.py +1 -5
  65. oscura/hardware/acquisition/visa.py +6 -6
  66. oscura/hardware/security/side_channel_detector.py +5 -508
  67. oscura/inference/message_format.py +686 -1
  68. oscura/jupyter/display.py +2 -2
  69. oscura/jupyter/magic.py +3 -3
  70. oscura/loaders/__init__.py +17 -12
  71. oscura/loaders/binary.py +1 -1
  72. oscura/loaders/chipwhisperer.py +1 -2
  73. oscura/loaders/configurable.py +1 -1
  74. oscura/loaders/csv_loader.py +2 -2
  75. oscura/loaders/hdf5_loader.py +1 -1
  76. oscura/loaders/lazy.py +6 -1
  77. oscura/loaders/mmap_loader.py +0 -1
  78. oscura/loaders/numpy_loader.py +8 -7
  79. oscura/loaders/preprocessing.py +3 -5
  80. oscura/loaders/rigol.py +21 -7
  81. oscura/loaders/sigrok.py +2 -5
  82. oscura/loaders/tdms.py +3 -2
  83. oscura/loaders/tektronix.py +38 -32
  84. oscura/loaders/tss.py +20 -27
  85. oscura/loaders/vcd.py +13 -8
  86. oscura/loaders/wav.py +1 -6
  87. oscura/pipeline/__init__.py +76 -0
  88. oscura/pipeline/handlers/__init__.py +165 -0
  89. oscura/pipeline/handlers/analyzers.py +1045 -0
  90. oscura/pipeline/handlers/decoders.py +899 -0
  91. oscura/pipeline/handlers/exporters.py +1103 -0
  92. oscura/pipeline/handlers/filters.py +891 -0
  93. oscura/pipeline/handlers/loaders.py +640 -0
  94. oscura/pipeline/handlers/transforms.py +768 -0
  95. oscura/reporting/__init__.py +88 -1
  96. oscura/reporting/automation.py +348 -0
  97. oscura/reporting/citations.py +374 -0
  98. oscura/reporting/core.py +54 -0
  99. oscura/reporting/formatting/__init__.py +11 -0
  100. oscura/reporting/formatting/measurements.py +320 -0
  101. oscura/reporting/html.py +57 -0
  102. oscura/reporting/interpretation.py +431 -0
  103. oscura/reporting/summary.py +329 -0
  104. oscura/reporting/templates/enhanced/protocol_re.html +504 -503
  105. oscura/reporting/visualization.py +542 -0
  106. oscura/side_channel/__init__.py +38 -57
  107. oscura/utils/builders/signal_builder.py +5 -5
  108. oscura/utils/comparison/compare.py +7 -9
  109. oscura/utils/comparison/golden.py +1 -1
  110. oscura/utils/filtering/convenience.py +2 -2
  111. oscura/utils/math/arithmetic.py +38 -62
  112. oscura/utils/math/interpolation.py +20 -20
  113. oscura/utils/pipeline/__init__.py +4 -17
  114. oscura/utils/progressive.py +1 -4
  115. oscura/utils/triggering/edge.py +1 -1
  116. oscura/utils/triggering/pattern.py +2 -2
  117. oscura/utils/triggering/pulse.py +2 -2
  118. oscura/utils/triggering/window.py +3 -3
  119. oscura/validation/hil_testing.py +11 -11
  120. oscura/visualization/__init__.py +47 -284
  121. oscura/visualization/batch.py +160 -0
  122. oscura/visualization/plot.py +542 -53
  123. oscura/visualization/styles.py +184 -318
  124. oscura/workflows/__init__.py +2 -0
  125. oscura/workflows/batch/advanced.py +1 -1
  126. oscura/workflows/batch/aggregate.py +7 -8
  127. oscura/workflows/complete_re.py +251 -23
  128. oscura/workflows/digital.py +27 -4
  129. oscura/workflows/multi_trace.py +136 -17
  130. oscura/workflows/waveform.py +788 -0
  131. {oscura-0.7.0.dist-info → oscura-0.10.0.dist-info}/METADATA +59 -79
  132. {oscura-0.7.0.dist-info → oscura-0.10.0.dist-info}/RECORD +135 -149
  133. oscura/side_channel/dpa.py +0 -1025
  134. oscura/utils/optimization/__init__.py +0 -19
  135. oscura/utils/optimization/parallel.py +0 -443
  136. oscura/utils/optimization/search.py +0 -532
  137. oscura/utils/pipeline/base.py +0 -338
  138. oscura/utils/pipeline/composition.py +0 -248
  139. oscura/utils/pipeline/parallel.py +0 -449
  140. oscura/utils/pipeline/pipeline.py +0 -375
  141. oscura/utils/search/__init__.py +0 -16
  142. oscura/utils/search/anomaly.py +0 -424
  143. oscura/utils/search/context.py +0 -294
  144. oscura/utils/search/pattern.py +0 -288
  145. oscura/utils/storage/__init__.py +0 -61
  146. oscura/utils/storage/database.py +0 -1166
  147. oscura/visualization/accessibility.py +0 -526
  148. oscura/visualization/annotations.py +0 -371
  149. oscura/visualization/axis_scaling.py +0 -305
  150. oscura/visualization/colors.py +0 -451
  151. oscura/visualization/digital.py +0 -436
  152. oscura/visualization/eye.py +0 -571
  153. oscura/visualization/histogram.py +0 -281
  154. oscura/visualization/interactive.py +0 -1035
  155. oscura/visualization/jitter.py +0 -1042
  156. oscura/visualization/keyboard.py +0 -394
  157. oscura/visualization/layout.py +0 -400
  158. oscura/visualization/optimization.py +0 -1079
  159. oscura/visualization/palettes.py +0 -446
  160. oscura/visualization/power.py +0 -508
  161. oscura/visualization/power_extended.py +0 -955
  162. oscura/visualization/presets.py +0 -469
  163. oscura/visualization/protocols.py +0 -1246
  164. oscura/visualization/render.py +0 -223
  165. oscura/visualization/rendering.py +0 -444
  166. oscura/visualization/reverse_engineering.py +0 -838
  167. oscura/visualization/signal_integrity.py +0 -989
  168. oscura/visualization/specialized.py +0 -643
  169. oscura/visualization/spectral.py +0 -1226
  170. oscura/visualization/thumbnails.py +0 -340
  171. oscura/visualization/time_axis.py +0 -351
  172. oscura/visualization/waveform.py +0 -454
  173. {oscura-0.7.0.dist-info → oscura-0.10.0.dist-info}/WHEEL +0 -0
  174. {oscura-0.7.0.dist-info → oscura-0.10.0.dist-info}/entry_points.txt +0 -0
  175. {oscura-0.7.0.dist-info → oscura-0.10.0.dist-info}/licenses/LICENSE +0 -0
@@ -266,7 +266,12 @@
266
266
  "category": "Powertrain",
267
267
  "severity": "High",
268
268
  "system": "Throttle Control",
269
- "possible_causes": ["TPS circuit shorted to voltage", "Faulty TPS sensor", "Wiring harness open", "ECM problem"]
269
+ "possible_causes": [
270
+ "TPS circuit shorted to voltage",
271
+ "Faulty TPS sensor",
272
+ "Wiring harness open",
273
+ "ECM problem"
274
+ ]
270
275
  },
271
276
  "P0125": {
272
277
  "code": "P0125",
@@ -860,7 +865,12 @@
860
865
  "category": "Powertrain",
861
866
  "severity": "Medium",
862
867
  "system": "Emissions Control",
863
- "possible_causes": ["EGR valve stuck closed", "EGR passages clogged", "Faulty EGR valve", "Vacuum leak"]
868
+ "possible_causes": [
869
+ "EGR valve stuck closed",
870
+ "EGR passages clogged",
871
+ "Faulty EGR valve",
872
+ "Vacuum leak"
873
+ ]
864
874
  },
865
875
  "P0401": {
866
876
  "code": "P0401",
@@ -881,7 +891,12 @@
881
891
  "category": "Powertrain",
882
892
  "severity": "Medium",
883
893
  "system": "Emissions Control",
884
- "possible_causes": ["EGR valve stuck open", "Faulty EGR valve", "EGR vacuum solenoid fault", "ECM problem"]
894
+ "possible_causes": [
895
+ "EGR valve stuck open",
896
+ "Faulty EGR valve",
897
+ "EGR vacuum solenoid fault",
898
+ "ECM problem"
899
+ ]
885
900
  },
886
901
  "P0403": {
887
902
  "code": "P0403",
@@ -943,7 +958,12 @@
943
958
  "category": "Powertrain",
944
959
  "severity": "Low",
945
960
  "system": "Emissions Control",
946
- "possible_causes": ["Loose or missing fuel cap", "EVAP system leak", "Faulty purge valve", "Faulty vent valve"]
961
+ "possible_causes": [
962
+ "Loose or missing fuel cap",
963
+ "EVAP system leak",
964
+ "Faulty purge valve",
965
+ "Faulty vent valve"
966
+ ]
947
967
  },
948
968
  "P0441": {
949
969
  "code": "P0441",
@@ -1055,7 +1075,12 @@
1055
1075
  "category": "Powertrain",
1056
1076
  "severity": "Low",
1057
1077
  "system": "Idle Control",
1058
- "possible_causes": ["Vacuum leak", "IAC valve fault", "Dirty throttle body", "PCV valve problem"]
1078
+ "possible_causes": [
1079
+ "Vacuum leak",
1080
+ "IAC valve fault",
1081
+ "Dirty throttle body",
1082
+ "PCV valve problem"
1083
+ ]
1059
1084
  },
1060
1085
  "P0507": {
1061
1086
  "code": "P0507",
@@ -1063,7 +1088,12 @@
1063
1088
  "category": "Powertrain",
1064
1089
  "severity": "Low",
1065
1090
  "system": "Idle Control",
1066
- "possible_causes": ["Vacuum leak", "IAC valve stuck open", "PCV valve stuck open", "EVAP purge valve leaking"]
1091
+ "possible_causes": [
1092
+ "Vacuum leak",
1093
+ "IAC valve stuck open",
1094
+ "PCV valve stuck open",
1095
+ "EVAP purge valve leaking"
1096
+ ]
1067
1097
  },
1068
1098
  "P0600": {
1069
1099
  "code": "P0600",
@@ -1097,7 +1127,12 @@
1097
1127
  "category": "Powertrain",
1098
1128
  "severity": "Critical",
1099
1129
  "system": "Engine Control Module",
1100
- "possible_causes": ["ECM not programmed", "ECM programming incomplete", "Wrong software version", "ECM fault"]
1130
+ "possible_causes": [
1131
+ "ECM not programmed",
1132
+ "ECM programming incomplete",
1133
+ "Wrong software version",
1134
+ "ECM fault"
1135
+ ]
1101
1136
  },
1102
1137
  "P0603": {
1103
1138
  "code": "P0603",
@@ -1170,7 +1205,12 @@
1170
1205
  "category": "Powertrain",
1171
1206
  "severity": "Medium",
1172
1207
  "system": "Charging System",
1173
- "possible_causes": ["Faulty alternator", "Wiring harness problem", "Poor electrical connection", "ECM fault"]
1208
+ "possible_causes": [
1209
+ "Faulty alternator",
1210
+ "Wiring harness problem",
1211
+ "Poor electrical connection",
1212
+ "ECM fault"
1213
+ ]
1174
1214
  },
1175
1215
  "P0625": {
1176
1216
  "code": "P0625",
@@ -1243,7 +1283,12 @@
1243
1283
  "category": "Powertrain",
1244
1284
  "severity": "High",
1245
1285
  "system": "Transmission",
1246
- "possible_causes": ["Faulty input speed sensor", "Wiring harness problem", "Sensor reluctor damaged", "TCM fault"]
1286
+ "possible_causes": [
1287
+ "Faulty input speed sensor",
1288
+ "Wiring harness problem",
1289
+ "Sensor reluctor damaged",
1290
+ "TCM fault"
1291
+ ]
1247
1292
  },
1248
1293
  "P0720": {
1249
1294
  "code": "P0720",
@@ -1446,7 +1491,12 @@
1446
1491
  "category": "Chassis",
1447
1492
  "severity": "High",
1448
1493
  "system": "ABS",
1449
- "possible_causes": ["Faulty valve relay", "Relay circuit problem", "ABS module fault", "Wiring harness issue"]
1494
+ "possible_causes": [
1495
+ "Faulty valve relay",
1496
+ "Relay circuit problem",
1497
+ "ABS module fault",
1498
+ "Wiring harness issue"
1499
+ ]
1450
1500
  },
1451
1501
  "C0161": {
1452
1502
  "code": "C0161",
@@ -2156,7 +2206,12 @@
2156
2206
  "category": "Body",
2157
2207
  "severity": "Low",
2158
2208
  "system": "Lighting System",
2159
- "possible_causes": ["Burned out bulb", "Wiring harness problem", "Lamp socket corrosion", "BCM fault"]
2209
+ "possible_causes": [
2210
+ "Burned out bulb",
2211
+ "Wiring harness problem",
2212
+ "Lamp socket corrosion",
2213
+ "BCM fault"
2214
+ ]
2160
2215
  },
2161
2216
  "B0601": {
2162
2217
  "code": "B0601",
@@ -2177,7 +2232,12 @@
2177
2232
  "category": "Body",
2178
2233
  "severity": "Low",
2179
2234
  "system": "Lighting System",
2180
- "possible_causes": ["Burned out turn signal bulb", "Wiring harness problem", "Flasher relay fault", "BCM fault"]
2235
+ "possible_causes": [
2236
+ "Burned out turn signal bulb",
2237
+ "Wiring harness problem",
2238
+ "Flasher relay fault",
2239
+ "BCM fault"
2240
+ ]
2181
2241
  },
2182
2242
  "B0603": {
2183
2243
  "code": "B0603",
@@ -2185,7 +2245,12 @@
2185
2245
  "category": "Body",
2186
2246
  "severity": "Low",
2187
2247
  "system": "Lighting System",
2188
- "possible_causes": ["Burned out turn signal bulb", "Wiring harness problem", "Flasher relay fault", "BCM fault"]
2248
+ "possible_causes": [
2249
+ "Burned out turn signal bulb",
2250
+ "Wiring harness problem",
2251
+ "Flasher relay fault",
2252
+ "BCM fault"
2253
+ ]
2189
2254
  },
2190
2255
  "B0604": {
2191
2256
  "code": "B0604",
@@ -2297,7 +2362,12 @@
2297
2362
  "category": "Body",
2298
2363
  "severity": "Low",
2299
2364
  "system": "Keyless Entry",
2300
- "possible_causes": ["Key fob battery weak", "Key fob not synchronized", "BCM fault", "Receiver antenna fault"]
2365
+ "possible_causes": [
2366
+ "Key fob battery weak",
2367
+ "Key fob not synchronized",
2368
+ "BCM fault",
2369
+ "Receiver antenna fault"
2370
+ ]
2301
2371
  },
2302
2372
  "B1300": {
2303
2373
  "code": "B1300",
@@ -2396,7 +2466,12 @@
2396
2466
  "category": "Network",
2397
2467
  "severity": "Critical",
2398
2468
  "system": "CAN Bus",
2399
- "possible_causes": ["TCM not powered", "CAN bus wiring problem", "TCM internal fault", "CAN bus short circuit"]
2469
+ "possible_causes": [
2470
+ "TCM not powered",
2471
+ "CAN bus wiring problem",
2472
+ "TCM internal fault",
2473
+ "CAN bus short circuit"
2474
+ ]
2400
2475
  },
2401
2476
  "U0102": {
2402
2477
  "code": "U0102",
@@ -2469,7 +2544,12 @@
2469
2544
  "category": "Network",
2470
2545
  "severity": "High",
2471
2546
  "system": "CAN Bus",
2472
- "possible_causes": ["BCM not powered", "CAN bus wiring problem", "BCM internal fault", "Ground connection issue"]
2547
+ "possible_causes": [
2548
+ "BCM not powered",
2549
+ "CAN bus wiring problem",
2550
+ "BCM internal fault",
2551
+ "Ground connection issue"
2552
+ ]
2473
2553
  },
2474
2554
  "U0141": {
2475
2555
  "code": "U0141",
@@ -2477,7 +2557,12 @@
2477
2557
  "category": "Network",
2478
2558
  "severity": "High",
2479
2559
  "system": "CAN Bus",
2480
- "possible_causes": ["BCM not powered", "CAN bus wiring problem", "Module internal fault", "Connector problem"]
2560
+ "possible_causes": [
2561
+ "BCM not powered",
2562
+ "CAN bus wiring problem",
2563
+ "Module internal fault",
2564
+ "Connector problem"
2565
+ ]
2481
2566
  },
2482
2567
  "U0151": {
2483
2568
  "code": "U0151",
@@ -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