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
@@ -0,0 +1,899 @@
1
+ """Protocol decoder handlers for pipeline system.
2
+
3
+ This module provides handlers for decoding various communication protocols.
4
+ All handlers follow the standard signature: (inputs, params, step_name) -> outputs.
5
+
6
+ Available Handlers:
7
+ - decoder.uart: Decode UART/RS232 serial protocol
8
+ - decoder.spi: Decode SPI (Serial Peripheral Interface)
9
+ - decoder.i2c: Decode I2C (Inter-Integrated Circuit)
10
+ - decoder.can: Decode CAN bus protocol
11
+ - decoder.can_fd: Decode CAN-FD protocol
12
+ - decoder.lin: Decode LIN bus protocol
13
+ - decoder.flexray: Decode FlexRay protocol
14
+ - decoder.onewire: Decode 1-Wire protocol
15
+ - decoder.manchester: Decode Manchester encoded data
16
+ - decoder.i2s: Decode I2S audio protocol
17
+ - decoder.jtag: Decode JTAG debug interface
18
+ - decoder.swd: Decode SWD (Serial Wire Debug)
19
+ - decoder.usb: Decode USB protocol
20
+ - decoder.hdlc: Decode HDLC frames
21
+ - decoder.auto: Auto-detect and decode protocol
22
+ - decoder.multi_protocol: Decode multiple protocols from multi-channel
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ from typing import Any
28
+
29
+ from oscura.core.config.pipeline import PipelineExecutionError
30
+ from oscura.pipeline.handlers import register_handler
31
+
32
+ # Lazy imports
33
+ _protocols = None
34
+ _convenience = None
35
+
36
+
37
+ def _get_protocols() -> Any:
38
+ """Lazy import protocols module."""
39
+ global _protocols
40
+ if _protocols is None:
41
+ from oscura.analyzers import protocols as _protocols_module
42
+
43
+ _protocols = _protocols_module
44
+ return _protocols
45
+
46
+
47
+ def _get_convenience() -> Any:
48
+ """Lazy import convenience module."""
49
+ global _convenience
50
+ if _convenience is None:
51
+ import oscura.convenience as _convenience_module
52
+
53
+ _convenience = _convenience_module
54
+ return _convenience
55
+
56
+
57
+ @register_handler("decoder.uart")
58
+ def handle_decoder_uart(
59
+ inputs: dict[str, Any], params: dict[str, Any], step_name: str
60
+ ) -> dict[str, Any]:
61
+ """Decode UART/RS232 serial protocol.
62
+
63
+ Inputs:
64
+ trace: WaveformTrace or DigitalTrace to decode
65
+
66
+ Parameters:
67
+ baud_rate (int): Baud rate in bps (e.g., 9600, 115200)
68
+ data_bits (int, optional): Number of data bits (default: 8)
69
+ parity (str, optional): Parity type ('none', 'even', 'odd') (default: 'none')
70
+ stop_bits (float, optional): Number of stop bits (default: 1.0)
71
+ invert (bool, optional): Invert signal polarity (default: False)
72
+
73
+ Outputs:
74
+ frames: List of decoded UART frames
75
+ num_frames: Number of frames decoded
76
+ errors: List of errors encountered
77
+ statistics: Decoding statistics dict
78
+ """
79
+ protocols = _get_protocols()
80
+
81
+ trace = inputs.get("trace")
82
+ if trace is None:
83
+ raise PipelineExecutionError(
84
+ "Missing required input 'trace'. Suggestion: Check step inputs",
85
+ step_name=step_name,
86
+ )
87
+
88
+ baud_rate = params.get("baud_rate")
89
+ if baud_rate is None:
90
+ raise PipelineExecutionError(
91
+ "Missing required parameter 'baud_rate'. Suggestion: Add 'baud_rate: 115200' to params",
92
+ step_name=step_name,
93
+ )
94
+
95
+ data_bits = params.get("data_bits", 8)
96
+ parity = params.get("parity", "none")
97
+ stop_bits = params.get("stop_bits", 1.0)
98
+ invert = params.get("invert", False)
99
+
100
+ try:
101
+ decoder = protocols.UARTDecoder(
102
+ baud_rate=baud_rate,
103
+ data_bits=data_bits,
104
+ parity=parity,
105
+ stop_bits=stop_bits,
106
+ invert=invert,
107
+ )
108
+ frames = decoder.decode(trace)
109
+
110
+ # Compute statistics
111
+ error_count = sum(1 for f in frames if getattr(f, "error", False))
112
+ statistics = {
113
+ "total_frames": len(frames),
114
+ "errors": error_count,
115
+ "success_rate": (len(frames) - error_count) / len(frames) if frames else 0,
116
+ }
117
+ errors = [f"Frame {i}: error" for i, f in enumerate(frames) if getattr(f, "error", False)]
118
+
119
+ except Exception as e:
120
+ raise PipelineExecutionError(f"UART decoding failed: {e}", step_name=step_name) from e
121
+
122
+ return {
123
+ "frames": frames,
124
+ "num_frames": len(frames),
125
+ "errors": errors,
126
+ "statistics": statistics,
127
+ }
128
+
129
+
130
+ @register_handler("decoder.spi")
131
+ def handle_decoder_spi(
132
+ inputs: dict[str, Any], params: dict[str, Any], step_name: str
133
+ ) -> dict[str, Any]:
134
+ """Decode SPI (Serial Peripheral Interface) protocol.
135
+
136
+ Inputs:
137
+ clk: Clock signal (DigitalTrace)
138
+ mosi: MOSI (Master Out Slave In) signal (DigitalTrace, optional)
139
+ miso: MISO (Master In Slave Out) signal (DigitalTrace, optional)
140
+ cs: Chip Select signal (DigitalTrace, optional)
141
+
142
+ Parameters:
143
+ clock_edge (str, optional): Capture edge ('rising' or 'falling') (default: 'rising')
144
+ bit_order (str, optional): Bit order ('msb' or 'lsb') (default: 'msb')
145
+ word_size (int, optional): Word size in bits (default: 8)
146
+ cs_active_low (bool, optional): CS is active low (default: True)
147
+
148
+ Outputs:
149
+ frames: List of decoded SPI frames
150
+ num_frames: Number of frames decoded
151
+ statistics: Decoding statistics dict
152
+ """
153
+ protocols = _get_protocols()
154
+
155
+ clk = inputs.get("clk")
156
+ if clk is None:
157
+ raise PipelineExecutionError(
158
+ "Missing required input 'clk'. Suggestion: Provide clock signal",
159
+ step_name=step_name,
160
+ )
161
+
162
+ mosi = inputs.get("mosi")
163
+ miso = inputs.get("miso")
164
+ cs = inputs.get("cs")
165
+
166
+ if mosi is None and miso is None:
167
+ raise PipelineExecutionError(
168
+ "At least one of 'mosi' or 'miso' required. Suggestion: Provide MOSI and/or MISO signals",
169
+ step_name=step_name,
170
+ )
171
+
172
+ clock_edge = params.get("clock_edge", "rising")
173
+ bit_order = params.get("bit_order", "msb")
174
+ word_size = params.get("word_size", 8)
175
+ cs_active_low = params.get("cs_active_low", True)
176
+
177
+ try:
178
+ decoder = protocols.SPIDecoder(
179
+ clock_edge=clock_edge,
180
+ bit_order=bit_order,
181
+ word_size=word_size,
182
+ cs_active_low=cs_active_low,
183
+ )
184
+ frames = decoder.decode(clk, mosi=mosi, miso=miso, cs=cs)
185
+
186
+ statistics = {
187
+ "total_frames": len(frames),
188
+ "total_bytes": sum(len(f.data) if hasattr(f, "data") else 0 for f in frames),
189
+ }
190
+
191
+ except Exception as e:
192
+ raise PipelineExecutionError(f"SPI decoding failed: {e}", step_name=step_name) from e
193
+
194
+ return {
195
+ "frames": frames,
196
+ "num_frames": len(frames),
197
+ "statistics": statistics,
198
+ }
199
+
200
+
201
+ @register_handler("decoder.i2c")
202
+ def handle_decoder_i2c(
203
+ inputs: dict[str, Any], params: dict[str, Any], step_name: str
204
+ ) -> dict[str, Any]:
205
+ """Decode I2C (Inter-Integrated Circuit) protocol.
206
+
207
+ Inputs:
208
+ scl: I2C clock signal (DigitalTrace)
209
+ sda: I2C data signal (DigitalTrace)
210
+
211
+ Parameters:
212
+ address_bits (int, optional): Address width (7 or 10) (default: 7)
213
+
214
+ Outputs:
215
+ frames: List of decoded I2C frames
216
+ num_frames: Number of frames decoded
217
+ addresses: Set of I2C addresses seen
218
+ statistics: Decoding statistics dict
219
+ """
220
+ protocols = _get_protocols()
221
+
222
+ scl = inputs.get("scl")
223
+ sda = inputs.get("sda")
224
+
225
+ if scl is None or sda is None:
226
+ raise PipelineExecutionError(
227
+ "Missing required inputs 'scl' and 'sda'. Suggestion: Provide both SCL and SDA signals",
228
+ step_name=step_name,
229
+ )
230
+
231
+ address_bits = params.get("address_bits", 7)
232
+
233
+ try:
234
+ decoder = protocols.I2CDecoder(address_bits=address_bits)
235
+ frames = decoder.decode(scl, sda)
236
+
237
+ # Extract addresses
238
+ addresses = set()
239
+ for f in frames:
240
+ if hasattr(f, "address"):
241
+ addresses.add(f.address)
242
+
243
+ statistics = {
244
+ "total_frames": len(frames),
245
+ "unique_addresses": len(addresses),
246
+ }
247
+
248
+ except Exception as e:
249
+ raise PipelineExecutionError(f"I2C decoding failed: {e}", step_name=step_name) from e
250
+
251
+ return {
252
+ "frames": frames,
253
+ "num_frames": len(frames),
254
+ "addresses": sorted(addresses),
255
+ "statistics": statistics,
256
+ }
257
+
258
+
259
+ @register_handler("decoder.can")
260
+ def handle_decoder_can(
261
+ inputs: dict[str, Any], params: dict[str, Any], step_name: str
262
+ ) -> dict[str, Any]:
263
+ """Decode CAN bus protocol.
264
+
265
+ Inputs:
266
+ trace: CAN bus signal (WaveformTrace or DigitalTrace)
267
+
268
+ Parameters:
269
+ bit_rate (int): CAN bit rate in bps (e.g., 125000, 250000, 500000, 1000000)
270
+ sample_point (float, optional): Sample point (0-1) (default: 0.75)
271
+
272
+ Outputs:
273
+ frames: List of decoded CAN frames
274
+ num_frames: Number of frames decoded
275
+ can_ids: Set of CAN IDs seen
276
+ statistics: Decoding statistics dict
277
+ """
278
+ protocols = _get_protocols()
279
+
280
+ trace = inputs.get("trace")
281
+ if trace is None:
282
+ raise PipelineExecutionError("Missing required input 'trace'", step_name=step_name)
283
+
284
+ bit_rate = params.get("bit_rate")
285
+ if bit_rate is None:
286
+ raise PipelineExecutionError(
287
+ "Missing required parameter 'bit_rate'. Suggestion: Add 'bit_rate: 500000' to params",
288
+ step_name=step_name,
289
+ )
290
+
291
+ sample_point = params.get("sample_point", 0.75)
292
+
293
+ try:
294
+ decoder = protocols.CANDecoder(bit_rate=bit_rate, sample_point=sample_point)
295
+ frames = decoder.decode(trace)
296
+
297
+ # Extract CAN IDs
298
+ can_ids = set()
299
+ for f in frames:
300
+ if hasattr(f, "can_id"):
301
+ can_ids.add(f.can_id)
302
+
303
+ statistics = {
304
+ "total_frames": len(frames),
305
+ "unique_ids": len(can_ids),
306
+ }
307
+
308
+ except Exception as e:
309
+ raise PipelineExecutionError(f"CAN decoding failed: {e}", step_name=step_name) from e
310
+
311
+ return {
312
+ "frames": frames,
313
+ "num_frames": len(frames),
314
+ "can_ids": sorted(can_ids),
315
+ "statistics": statistics,
316
+ }
317
+
318
+
319
+ @register_handler("decoder.can_fd")
320
+ def handle_decoder_can_fd(
321
+ inputs: dict[str, Any], params: dict[str, Any], step_name: str
322
+ ) -> dict[str, Any]:
323
+ """Decode CAN-FD protocol.
324
+
325
+ Inputs:
326
+ trace: CAN-FD bus signal
327
+
328
+ Parameters:
329
+ nominal_bit_rate (int): Nominal bit rate in bps
330
+ data_bit_rate (int): Data phase bit rate in bps
331
+ sample_point (float, optional): Sample point (0-1) (default: 0.75)
332
+
333
+ Outputs:
334
+ frames: List of decoded CAN-FD frames
335
+ num_frames: Number of frames decoded
336
+ statistics: Decoding statistics dict
337
+ """
338
+ protocols = _get_protocols()
339
+
340
+ trace = inputs.get("trace")
341
+ if trace is None:
342
+ raise PipelineExecutionError("Missing required input 'trace'", step_name=step_name)
343
+
344
+ nominal_bit_rate = params.get("nominal_bit_rate")
345
+ data_bit_rate = params.get("data_bit_rate")
346
+
347
+ if nominal_bit_rate is None or data_bit_rate is None:
348
+ raise PipelineExecutionError(
349
+ "Missing required parameters 'nominal_bit_rate' and 'data_bit_rate'",
350
+ step_name=step_name,
351
+ )
352
+
353
+ sample_point = params.get("sample_point", 0.75)
354
+
355
+ try:
356
+ decoder = protocols.CANFDDecoder(
357
+ nominal_bit_rate=nominal_bit_rate,
358
+ data_bit_rate=data_bit_rate,
359
+ sample_point=sample_point,
360
+ )
361
+ frames = decoder.decode(trace)
362
+
363
+ statistics = {"total_frames": len(frames)}
364
+
365
+ except Exception as e:
366
+ raise PipelineExecutionError(f"CAN-FD decoding failed: {e}", step_name=step_name) from e
367
+
368
+ return {
369
+ "frames": frames,
370
+ "num_frames": len(frames),
371
+ "statistics": statistics,
372
+ }
373
+
374
+
375
+ @register_handler("decoder.lin")
376
+ def handle_decoder_lin(
377
+ inputs: dict[str, Any], params: dict[str, Any], step_name: str
378
+ ) -> dict[str, Any]:
379
+ """Decode LIN bus protocol.
380
+
381
+ Inputs:
382
+ trace: LIN bus signal
383
+
384
+ Parameters:
385
+ baud_rate (int): LIN baud rate in bps (typically 9600 or 19200)
386
+
387
+ Outputs:
388
+ frames: List of decoded LIN frames
389
+ num_frames: Number of frames decoded
390
+ statistics: Decoding statistics dict
391
+ """
392
+ protocols = _get_protocols()
393
+
394
+ trace = inputs.get("trace")
395
+ if trace is None:
396
+ raise PipelineExecutionError("Missing required input 'trace'", step_name=step_name)
397
+
398
+ baud_rate = params.get("baud_rate")
399
+ if baud_rate is None:
400
+ raise PipelineExecutionError("Missing required parameter 'baud_rate'", step_name=step_name)
401
+
402
+ try:
403
+ decoder = protocols.LINDecoder(baud_rate=baud_rate)
404
+ frames = decoder.decode(trace)
405
+
406
+ statistics = {"total_frames": len(frames)}
407
+
408
+ except Exception as e:
409
+ raise PipelineExecutionError(f"LIN decoding failed: {e}", step_name=step_name) from e
410
+
411
+ return {
412
+ "frames": frames,
413
+ "num_frames": len(frames),
414
+ "statistics": statistics,
415
+ }
416
+
417
+
418
+ @register_handler("decoder.flexray")
419
+ def handle_decoder_flexray(
420
+ inputs: dict[str, Any], params: dict[str, Any], step_name: str
421
+ ) -> dict[str, Any]:
422
+ """Decode FlexRay protocol.
423
+
424
+ Inputs:
425
+ trace: FlexRay bus signal
426
+
427
+ Parameters:
428
+ channel (str): FlexRay channel ('A' or 'B')
429
+ bit_rate (int): Bit rate in bps (typically 10000000)
430
+
431
+ Outputs:
432
+ frames: List of decoded FlexRay frames
433
+ num_frames: Number of frames decoded
434
+ statistics: Decoding statistics dict
435
+ """
436
+ protocols = _get_protocols()
437
+
438
+ trace = inputs.get("trace")
439
+ if trace is None:
440
+ raise PipelineExecutionError("Missing required input 'trace'", step_name=step_name)
441
+
442
+ channel = params.get("channel", "A")
443
+ bit_rate = params.get("bit_rate", 10000000)
444
+
445
+ try:
446
+ decoder = protocols.FlexRayDecoder(channel=channel, bit_rate=bit_rate)
447
+ frames = decoder.decode(trace)
448
+
449
+ statistics = {"total_frames": len(frames)}
450
+
451
+ except Exception as e:
452
+ raise PipelineExecutionError(f"FlexRay decoding failed: {e}", step_name=step_name) from e
453
+
454
+ return {
455
+ "frames": frames,
456
+ "num_frames": len(frames),
457
+ "statistics": statistics,
458
+ }
459
+
460
+
461
+ @register_handler("decoder.onewire")
462
+ def handle_decoder_onewire(
463
+ inputs: dict[str, Any], params: dict[str, Any], step_name: str
464
+ ) -> dict[str, Any]:
465
+ """Decode 1-Wire protocol.
466
+
467
+ Inputs:
468
+ trace: 1-Wire bus signal
469
+
470
+ Parameters:
471
+ None
472
+
473
+ Outputs:
474
+ frames: List of decoded 1-Wire frames
475
+ num_frames: Number of frames decoded
476
+ statistics: Decoding statistics dict
477
+ """
478
+ protocols = _get_protocols()
479
+
480
+ trace = inputs.get("trace")
481
+ if trace is None:
482
+ raise PipelineExecutionError("Missing required input 'trace'", step_name=step_name)
483
+
484
+ try:
485
+ decoder = protocols.OneWireDecoder()
486
+ frames = decoder.decode(trace)
487
+
488
+ statistics = {"total_frames": len(frames)}
489
+
490
+ except Exception as e:
491
+ raise PipelineExecutionError(f"1-Wire decoding failed: {e}", step_name=step_name) from e
492
+
493
+ return {
494
+ "frames": frames,
495
+ "num_frames": len(frames),
496
+ "statistics": statistics,
497
+ }
498
+
499
+
500
+ @register_handler("decoder.manchester")
501
+ def handle_decoder_manchester(
502
+ inputs: dict[str, Any], params: dict[str, Any], step_name: str
503
+ ) -> dict[str, Any]:
504
+ """Decode Manchester encoded data.
505
+
506
+ Inputs:
507
+ trace: Manchester encoded signal
508
+
509
+ Parameters:
510
+ bit_rate (int): Bit rate in bps
511
+ ieee_convention (bool, optional): Use IEEE 802.3 convention (default: True)
512
+
513
+ Outputs:
514
+ frames: List of decoded frames
515
+ num_frames: Number of frames decoded
516
+ data: Decoded bit stream
517
+ statistics: Decoding statistics dict
518
+ """
519
+ protocols = _get_protocols()
520
+
521
+ trace = inputs.get("trace")
522
+ if trace is None:
523
+ raise PipelineExecutionError("Missing required input 'trace'", step_name=step_name)
524
+
525
+ bit_rate = params.get("bit_rate")
526
+ if bit_rate is None:
527
+ raise PipelineExecutionError("Missing required parameter 'bit_rate'", step_name=step_name)
528
+
529
+ ieee_convention = params.get("ieee_convention", True)
530
+
531
+ try:
532
+ decoder = protocols.ManchesterDecoder(bit_rate=bit_rate, ieee_convention=ieee_convention)
533
+ frames = decoder.decode(trace)
534
+ data = b"".join(f.data if hasattr(f, "data") else b"" for f in frames)
535
+
536
+ statistics = {
537
+ "total_frames": len(frames),
538
+ "total_bits": len(data) * 8,
539
+ }
540
+
541
+ except Exception as e:
542
+ raise PipelineExecutionError(f"Manchester decoding failed: {e}", step_name=step_name) from e
543
+
544
+ return {
545
+ "frames": frames,
546
+ "num_frames": len(frames),
547
+ "data": data,
548
+ "statistics": statistics,
549
+ }
550
+
551
+
552
+ @register_handler("decoder.i2s")
553
+ def handle_decoder_i2s(
554
+ inputs: dict[str, Any], params: dict[str, Any], step_name: str
555
+ ) -> dict[str, Any]:
556
+ """Decode I2S (Inter-IC Sound) audio protocol.
557
+
558
+ Inputs:
559
+ sck: Serial clock (bit clock)
560
+ ws: Word select (frame clock)
561
+ sd: Serial data
562
+
563
+ Parameters:
564
+ bits_per_sample (int, optional): Bits per sample (default: 16)
565
+
566
+ Outputs:
567
+ frames: List of decoded audio frames
568
+ num_frames: Number of frames decoded
569
+ statistics: Decoding statistics dict
570
+ """
571
+ protocols = _get_protocols()
572
+
573
+ sck = inputs.get("sck")
574
+ ws = inputs.get("ws")
575
+ sd = inputs.get("sd")
576
+
577
+ if sck is None or ws is None or sd is None:
578
+ raise PipelineExecutionError(
579
+ "Missing required inputs 'sck', 'ws', 'sd'", step_name=step_name
580
+ )
581
+
582
+ bits_per_sample = params.get("bits_per_sample", 16)
583
+
584
+ try:
585
+ decoder = protocols.I2SDecoder(bits_per_sample=bits_per_sample)
586
+ frames = decoder.decode(sck, ws, sd)
587
+
588
+ statistics = {"total_frames": len(frames)}
589
+
590
+ except Exception as e:
591
+ raise PipelineExecutionError(f"I2S decoding failed: {e}", step_name=step_name) from e
592
+
593
+ return {
594
+ "frames": frames,
595
+ "num_frames": len(frames),
596
+ "statistics": statistics,
597
+ }
598
+
599
+
600
+ @register_handler("decoder.jtag")
601
+ def handle_decoder_jtag(
602
+ inputs: dict[str, Any], params: dict[str, Any], step_name: str
603
+ ) -> dict[str, Any]:
604
+ """Decode JTAG debug interface.
605
+
606
+ Inputs:
607
+ tck: Test clock
608
+ tms: Test mode select
609
+ tdi: Test data in
610
+ tdo: Test data out (optional)
611
+
612
+ Parameters:
613
+ None
614
+
615
+ Outputs:
616
+ frames: List of decoded JTAG frames
617
+ num_frames: Number of frames decoded
618
+ statistics: Decoding statistics dict
619
+ """
620
+ protocols = _get_protocols()
621
+
622
+ tck = inputs.get("tck")
623
+ tms = inputs.get("tms")
624
+ tdi = inputs.get("tdi")
625
+ tdo = inputs.get("tdo")
626
+
627
+ if tck is None or tms is None or tdi is None:
628
+ raise PipelineExecutionError(
629
+ "Missing required inputs 'tck', 'tms', 'tdi'", step_name=step_name
630
+ )
631
+
632
+ try:
633
+ decoder = protocols.JTAGDecoder()
634
+ frames = decoder.decode(tck, tms, tdi, tdo)
635
+
636
+ statistics = {"total_frames": len(frames)}
637
+
638
+ except Exception as e:
639
+ raise PipelineExecutionError(f"JTAG decoding failed: {e}", step_name=step_name) from e
640
+
641
+ return {
642
+ "frames": frames,
643
+ "num_frames": len(frames),
644
+ "statistics": statistics,
645
+ }
646
+
647
+
648
+ @register_handler("decoder.swd")
649
+ def handle_decoder_swd(
650
+ inputs: dict[str, Any], params: dict[str, Any], step_name: str
651
+ ) -> dict[str, Any]:
652
+ """Decode SWD (Serial Wire Debug) protocol.
653
+
654
+ Inputs:
655
+ swclk: Serial wire clock
656
+ swdio: Serial wire data I/O
657
+
658
+ Parameters:
659
+ None
660
+
661
+ Outputs:
662
+ frames: List of decoded SWD frames
663
+ num_frames: Number of frames decoded
664
+ statistics: Decoding statistics dict
665
+ """
666
+ protocols = _get_protocols()
667
+
668
+ swclk = inputs.get("swclk")
669
+ swdio = inputs.get("swdio")
670
+
671
+ if swclk is None or swdio is None:
672
+ raise PipelineExecutionError(
673
+ "Missing required inputs 'swclk' and 'swdio'", step_name=step_name
674
+ )
675
+
676
+ try:
677
+ decoder = protocols.SWDDecoder()
678
+ frames = decoder.decode(swclk, swdio)
679
+
680
+ statistics = {"total_frames": len(frames)}
681
+
682
+ except Exception as e:
683
+ raise PipelineExecutionError(f"SWD decoding failed: {e}", step_name=step_name) from e
684
+
685
+ return {
686
+ "frames": frames,
687
+ "num_frames": len(frames),
688
+ "statistics": statistics,
689
+ }
690
+
691
+
692
+ @register_handler("decoder.usb")
693
+ def handle_decoder_usb(
694
+ inputs: dict[str, Any], params: dict[str, Any], step_name: str
695
+ ) -> dict[str, Any]:
696
+ """Decode USB protocol.
697
+
698
+ Inputs:
699
+ dp: USB D+ signal
700
+ dm: USB D- signal
701
+
702
+ Parameters:
703
+ speed (str, optional): USB speed ('low', 'full', 'high') (default: 'full')
704
+
705
+ Outputs:
706
+ frames: List of decoded USB packets
707
+ num_frames: Number of packets decoded
708
+ statistics: Decoding statistics dict
709
+ """
710
+ protocols = _get_protocols()
711
+
712
+ dp = inputs.get("dp")
713
+ dm = inputs.get("dm")
714
+
715
+ if dp is None or dm is None:
716
+ raise PipelineExecutionError("Missing required inputs 'dp' and 'dm'", step_name=step_name)
717
+
718
+ speed = params.get("speed", "full")
719
+
720
+ try:
721
+ decoder = protocols.USBDecoder(speed=speed)
722
+ frames = decoder.decode(dp, dm)
723
+
724
+ statistics = {"total_frames": len(frames)}
725
+
726
+ except Exception as e:
727
+ raise PipelineExecutionError(f"USB decoding failed: {e}", step_name=step_name) from e
728
+
729
+ return {
730
+ "frames": frames,
731
+ "num_frames": len(frames),
732
+ "statistics": statistics,
733
+ }
734
+
735
+
736
+ @register_handler("decoder.hdlc")
737
+ def handle_decoder_hdlc(
738
+ inputs: dict[str, Any], params: dict[str, Any], step_name: str
739
+ ) -> dict[str, Any]:
740
+ """Decode HDLC frames.
741
+
742
+ Inputs:
743
+ trace: HDLC signal
744
+
745
+ Parameters:
746
+ None
747
+
748
+ Outputs:
749
+ frames: List of decoded HDLC frames
750
+ num_frames: Number of frames decoded
751
+ statistics: Decoding statistics dict
752
+ """
753
+ protocols = _get_protocols()
754
+
755
+ trace = inputs.get("trace")
756
+ if trace is None:
757
+ raise PipelineExecutionError("Missing required input 'trace'", step_name=step_name)
758
+
759
+ try:
760
+ decoder = protocols.HDLCDecoder()
761
+ frames = decoder.decode(trace)
762
+
763
+ statistics = {"total_frames": len(frames)}
764
+
765
+ except Exception as e:
766
+ raise PipelineExecutionError(f"HDLC decoding failed: {e}", step_name=step_name) from e
767
+
768
+ return {
769
+ "frames": frames,
770
+ "num_frames": len(frames),
771
+ "statistics": statistics,
772
+ }
773
+
774
+
775
+ @register_handler("decoder.auto")
776
+ def handle_decoder_auto(
777
+ inputs: dict[str, Any], params: dict[str, Any], step_name: str
778
+ ) -> dict[str, Any]:
779
+ """Auto-detect and decode protocol.
780
+
781
+ Inputs:
782
+ trace: WaveformTrace or DigitalTrace
783
+
784
+ Parameters:
785
+ protocol (str, optional): Force specific protocol (None for auto-detect)
786
+ min_confidence (float, optional): Minimum detection confidence (0-1) (default: 0.5)
787
+
788
+ Outputs:
789
+ protocol: Detected protocol name
790
+ frames: List of decoded frames
791
+ confidence: Detection confidence (0-1)
792
+ baud_rate: Detected baud/bit rate (if applicable)
793
+ config: Protocol configuration parameters
794
+ errors: List of decoding errors
795
+ statistics: Decoding statistics
796
+ """
797
+ convenience = _get_convenience()
798
+
799
+ trace = inputs.get("trace")
800
+ if trace is None:
801
+ raise PipelineExecutionError("Missing required input 'trace'", step_name=step_name)
802
+
803
+ protocol = params.get("protocol")
804
+ min_confidence = params.get("min_confidence", 0.5)
805
+
806
+ try:
807
+ result = convenience.auto_decode(trace, protocol=protocol, min_confidence=min_confidence)
808
+
809
+ # Extract all fields from DecodeResult
810
+ return {
811
+ "protocol": result.protocol,
812
+ "frames": result.frames,
813
+ "confidence": result.confidence,
814
+ "baud_rate": result.baud_rate,
815
+ "config": result.config,
816
+ "errors": result.errors,
817
+ "statistics": result.statistics,
818
+ }
819
+
820
+ except Exception as e:
821
+ raise PipelineExecutionError(f"Auto-decode failed: {e}", step_name=step_name) from e
822
+
823
+
824
+ @register_handler("decoder.multi_protocol")
825
+ def handle_decoder_multi_protocol(
826
+ inputs: dict[str, Any], params: dict[str, Any], step_name: str
827
+ ) -> dict[str, Any]:
828
+ """Decode multiple protocols from multi-channel trace.
829
+
830
+ Inputs:
831
+ channels: Dict mapping channel names to traces
832
+
833
+ Parameters:
834
+ protocols (dict): Dict mapping protocol names to decoder configs
835
+ Example: {"uart": {"channel": "CH1", "baud_rate": 115200},
836
+ "spi": {"clk": "CH2", "mosi": "CH3"}}
837
+
838
+ Outputs:
839
+ results: Dict mapping protocol names to decode results
840
+ summary: Summary statistics across all protocols
841
+ """
842
+ # This is a meta-handler that dispatches to other handlers
843
+ channels = inputs.get("channels")
844
+ if channels is None:
845
+ raise PipelineExecutionError("Missing required input 'channels'", step_name=step_name)
846
+
847
+ protocol_configs = params.get("protocols")
848
+ if not protocol_configs:
849
+ raise PipelineExecutionError(
850
+ "Missing required parameter 'protocols'. Suggestion: Specify protocols dict with decoder configs",
851
+ step_name=step_name,
852
+ )
853
+
854
+ results = {}
855
+ total_frames = 0
856
+
857
+ for proto_name, proto_config in protocol_configs.items():
858
+ try:
859
+ # Get handler for this protocol
860
+ from oscura.pipeline.handlers import get_handler
861
+
862
+ handler_type = f"decoder.{proto_name}"
863
+ handler = get_handler(handler_type)
864
+
865
+ if handler is None:
866
+ raise PipelineExecutionError(
867
+ f"Unknown protocol: {proto_name}. Suggestion: Supported protocols: uart, spi, i2c, can, etc.",
868
+ step_name=step_name,
869
+ )
870
+
871
+ # Map channel names to actual traces
872
+ channel_name = proto_config.get("channel")
873
+ if channel_name:
874
+ proto_inputs = {"trace": channels.get(channel_name)}
875
+ else:
876
+ # Multi-signal protocols (SPI, I2C, etc.)
877
+ proto_inputs = {}
878
+ for key in ["clk", "mosi", "miso", "cs", "scl", "sda", "dp", "dm"]:
879
+ if key in proto_config:
880
+ ch_name = proto_config[key]
881
+ proto_inputs[key] = channels.get(ch_name)
882
+
883
+ # Decode this protocol
884
+ result = handler(proto_inputs, proto_config, step_name)
885
+ results[proto_name] = result
886
+ total_frames += result.get("num_frames", 0)
887
+
888
+ except Exception as e:
889
+ results[proto_name] = {"error": str(e)}
890
+
891
+ summary = {
892
+ "protocols_decoded": len([r for r in results.values() if "error" not in r]),
893
+ "total_frames": total_frames,
894
+ }
895
+
896
+ return {
897
+ "results": results,
898
+ "summary": summary,
899
+ }