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
@@ -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
+ }