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.
- oscura/__init__.py +19 -19
- oscura/__main__.py +4 -0
- oscura/analyzers/__init__.py +2 -0
- oscura/analyzers/digital/extraction.py +2 -3
- oscura/analyzers/digital/quality.py +1 -1
- oscura/analyzers/digital/timing.py +1 -1
- oscura/analyzers/ml/signal_classifier.py +6 -0
- oscura/analyzers/patterns/__init__.py +66 -0
- oscura/analyzers/power/basic.py +3 -3
- oscura/analyzers/power/soa.py +1 -1
- oscura/analyzers/power/switching.py +3 -3
- oscura/analyzers/signal_classification.py +529 -0
- oscura/analyzers/signal_integrity/sparams.py +3 -3
- oscura/analyzers/statistics/basic.py +10 -7
- oscura/analyzers/validation.py +1 -1
- oscura/analyzers/waveform/measurements.py +200 -156
- oscura/analyzers/waveform/measurements_with_uncertainty.py +91 -35
- oscura/analyzers/waveform/spectral.py +182 -84
- oscura/api/dsl/commands.py +15 -6
- oscura/api/server/templates/base.html +137 -146
- oscura/api/server/templates/export.html +84 -110
- oscura/api/server/templates/home.html +248 -267
- oscura/api/server/templates/protocols.html +44 -48
- oscura/api/server/templates/reports.html +27 -35
- oscura/api/server/templates/session_detail.html +68 -78
- oscura/api/server/templates/sessions.html +62 -72
- oscura/api/server/templates/waveforms.html +54 -64
- oscura/automotive/__init__.py +1 -1
- oscura/automotive/can/session.py +1 -1
- oscura/automotive/dbc/generator.py +638 -23
- oscura/automotive/dtc/data.json +17 -102
- oscura/automotive/flexray/fibex.py +9 -1
- oscura/automotive/uds/decoder.py +99 -6
- oscura/cli/analyze.py +8 -2
- oscura/cli/batch.py +36 -5
- oscura/cli/characterize.py +18 -4
- oscura/cli/export.py +47 -5
- oscura/cli/main.py +2 -0
- oscura/cli/onboarding/wizard.py +10 -6
- oscura/cli/pipeline.py +585 -0
- oscura/cli/visualize.py +6 -4
- oscura/convenience.py +400 -32
- oscura/core/measurement_result.py +286 -0
- oscura/core/progress.py +1 -1
- oscura/core/schemas/device_mapping.json +2 -8
- oscura/core/schemas/packet_format.json +4 -24
- oscura/core/schemas/protocol_definition.json +2 -12
- oscura/core/types.py +232 -239
- oscura/correlation/multi_protocol.py +1 -1
- oscura/export/legacy/__init__.py +11 -0
- oscura/export/legacy/wav.py +75 -0
- oscura/exporters/__init__.py +19 -0
- oscura/exporters/wireshark.py +809 -0
- oscura/hardware/acquisition/file.py +5 -19
- oscura/hardware/acquisition/saleae.py +10 -10
- oscura/hardware/acquisition/socketcan.py +4 -6
- oscura/hardware/acquisition/synthetic.py +1 -5
- oscura/hardware/acquisition/visa.py +6 -6
- oscura/hardware/security/side_channel_detector.py +5 -508
- oscura/inference/message_format.py +686 -1
- oscura/jupyter/display.py +2 -2
- oscura/jupyter/magic.py +3 -3
- oscura/loaders/__init__.py +17 -12
- oscura/loaders/binary.py +1 -1
- oscura/loaders/chipwhisperer.py +1 -2
- oscura/loaders/configurable.py +1 -1
- oscura/loaders/csv_loader.py +2 -2
- oscura/loaders/hdf5_loader.py +1 -1
- oscura/loaders/lazy.py +6 -1
- oscura/loaders/mmap_loader.py +0 -1
- oscura/loaders/numpy_loader.py +8 -7
- oscura/loaders/preprocessing.py +3 -5
- oscura/loaders/rigol.py +21 -7
- oscura/loaders/sigrok.py +2 -5
- oscura/loaders/tdms.py +3 -2
- oscura/loaders/tektronix.py +38 -32
- oscura/loaders/tss.py +20 -27
- oscura/loaders/validation.py +17 -10
- oscura/loaders/vcd.py +13 -8
- oscura/loaders/wav.py +1 -6
- oscura/pipeline/__init__.py +76 -0
- oscura/pipeline/handlers/__init__.py +165 -0
- oscura/pipeline/handlers/analyzers.py +1045 -0
- oscura/pipeline/handlers/decoders.py +899 -0
- oscura/pipeline/handlers/exporters.py +1103 -0
- oscura/pipeline/handlers/filters.py +891 -0
- oscura/pipeline/handlers/loaders.py +640 -0
- oscura/pipeline/handlers/transforms.py +768 -0
- oscura/reporting/formatting/measurements.py +55 -14
- oscura/reporting/templates/enhanced/protocol_re.html +504 -503
- oscura/sessions/legacy.py +49 -1
- oscura/side_channel/__init__.py +38 -57
- oscura/utils/builders/signal_builder.py +5 -5
- oscura/utils/comparison/compare.py +7 -9
- oscura/utils/comparison/golden.py +1 -1
- oscura/utils/filtering/convenience.py +2 -2
- oscura/utils/math/arithmetic.py +38 -62
- oscura/utils/math/interpolation.py +20 -20
- oscura/utils/pipeline/__init__.py +4 -17
- oscura/utils/progressive.py +1 -4
- oscura/utils/triggering/edge.py +1 -1
- oscura/utils/triggering/pattern.py +2 -2
- oscura/utils/triggering/pulse.py +2 -2
- oscura/utils/triggering/window.py +3 -3
- oscura/validation/hil_testing.py +11 -11
- oscura/visualization/__init__.py +46 -284
- oscura/visualization/batch.py +72 -433
- oscura/visualization/plot.py +542 -53
- oscura/visualization/styles.py +184 -318
- oscura/workflows/batch/advanced.py +1 -1
- oscura/workflows/batch/aggregate.py +12 -9
- oscura/workflows/complete_re.py +251 -23
- oscura/workflows/digital.py +27 -4
- oscura/workflows/multi_trace.py +136 -17
- oscura/workflows/waveform.py +11 -6
- oscura-0.11.0.dist-info/METADATA +460 -0
- {oscura-0.8.0.dist-info → oscura-0.11.0.dist-info}/RECORD +120 -145
- oscura/side_channel/dpa.py +0 -1025
- oscura/utils/optimization/__init__.py +0 -19
- oscura/utils/optimization/parallel.py +0 -443
- oscura/utils/optimization/search.py +0 -532
- oscura/utils/pipeline/base.py +0 -338
- oscura/utils/pipeline/composition.py +0 -248
- oscura/utils/pipeline/parallel.py +0 -449
- oscura/utils/pipeline/pipeline.py +0 -375
- oscura/utils/search/__init__.py +0 -16
- oscura/utils/search/anomaly.py +0 -424
- oscura/utils/search/context.py +0 -294
- oscura/utils/search/pattern.py +0 -288
- oscura/utils/storage/__init__.py +0 -61
- oscura/utils/storage/database.py +0 -1166
- oscura/visualization/accessibility.py +0 -526
- oscura/visualization/annotations.py +0 -371
- oscura/visualization/axis_scaling.py +0 -305
- oscura/visualization/colors.py +0 -451
- oscura/visualization/digital.py +0 -436
- oscura/visualization/eye.py +0 -571
- oscura/visualization/histogram.py +0 -281
- oscura/visualization/interactive.py +0 -1035
- oscura/visualization/jitter.py +0 -1042
- oscura/visualization/keyboard.py +0 -394
- oscura/visualization/layout.py +0 -400
- oscura/visualization/optimization.py +0 -1079
- oscura/visualization/palettes.py +0 -446
- oscura/visualization/power.py +0 -508
- oscura/visualization/power_extended.py +0 -955
- oscura/visualization/presets.py +0 -469
- oscura/visualization/protocols.py +0 -1246
- oscura/visualization/render.py +0 -223
- oscura/visualization/rendering.py +0 -444
- oscura/visualization/reverse_engineering.py +0 -838
- oscura/visualization/signal_integrity.py +0 -989
- oscura/visualization/specialized.py +0 -643
- oscura/visualization/spectral.py +0 -1226
- oscura/visualization/thumbnails.py +0 -340
- oscura/visualization/time_axis.py +0 -351
- oscura/visualization/waveform.py +0 -454
- oscura-0.8.0.dist-info/METADATA +0 -661
- {oscura-0.8.0.dist-info → oscura-0.11.0.dist-info}/WHEEL +0 -0
- {oscura-0.8.0.dist-info → oscura-0.11.0.dist-info}/entry_points.txt +0 -0
- {oscura-0.8.0.dist-info → oscura-0.11.0.dist-info}/licenses/LICENSE +0 -0
oscura/automotive/dtc/data.json
CHANGED
|
@@ -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
|
-
|
|
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
|
oscura/automotive/uds/decoder.py
CHANGED
|
@@ -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
|
|
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
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
|
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"{
|
|
232
|
-
|
|
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),
|
|
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"{
|
|
182
|
-
|
|
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
|
-
|
|
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"
|
|
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
|
|
oscura/cli/characterize.py
CHANGED
|
@@ -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"{
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
"
|
|
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
|
-
|
|
205
|
-
|
|
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
|
|
oscura/cli/onboarding/wizard.py
CHANGED
|
@@ -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, "
|
|
228
|
-
print(f" Channel: {meta.
|
|
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
|
-
|
|
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 {
|
|
384
|
+
f"THD is {thd_value:.1f} dB - consider filtering to reduce distortion"
|
|
381
385
|
)
|
|
382
|
-
if
|
|
386
|
+
if snr_value is not None and snr_value < 40:
|
|
383
387
|
self.result.recommendations.append(
|
|
384
|
-
f"SNR is {
|
|
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
|