oscura 0.8.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 (151) 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/patterns/__init__.py +66 -0
  7. oscura/analyzers/power/basic.py +3 -3
  8. oscura/analyzers/power/soa.py +1 -1
  9. oscura/analyzers/power/switching.py +3 -3
  10. oscura/analyzers/signal_classification.py +529 -0
  11. oscura/analyzers/signal_integrity/sparams.py +3 -3
  12. oscura/analyzers/statistics/basic.py +10 -7
  13. oscura/analyzers/validation.py +1 -1
  14. oscura/analyzers/waveform/measurements.py +200 -156
  15. oscura/analyzers/waveform/measurements_with_uncertainty.py +91 -35
  16. oscura/analyzers/waveform/spectral.py +164 -73
  17. oscura/api/dsl/commands.py +15 -6
  18. oscura/api/server/templates/base.html +137 -146
  19. oscura/api/server/templates/export.html +84 -110
  20. oscura/api/server/templates/home.html +248 -267
  21. oscura/api/server/templates/protocols.html +44 -48
  22. oscura/api/server/templates/reports.html +27 -35
  23. oscura/api/server/templates/session_detail.html +68 -78
  24. oscura/api/server/templates/sessions.html +62 -72
  25. oscura/api/server/templates/waveforms.html +54 -64
  26. oscura/automotive/__init__.py +1 -1
  27. oscura/automotive/can/session.py +1 -1
  28. oscura/automotive/dbc/generator.py +638 -23
  29. oscura/automotive/uds/decoder.py +99 -6
  30. oscura/cli/analyze.py +8 -2
  31. oscura/cli/batch.py +36 -5
  32. oscura/cli/characterize.py +18 -4
  33. oscura/cli/export.py +47 -5
  34. oscura/cli/main.py +2 -0
  35. oscura/cli/onboarding/wizard.py +10 -6
  36. oscura/cli/pipeline.py +585 -0
  37. oscura/cli/visualize.py +6 -4
  38. oscura/convenience.py +400 -32
  39. oscura/core/measurement_result.py +286 -0
  40. oscura/core/progress.py +1 -1
  41. oscura/core/types.py +232 -239
  42. oscura/correlation/multi_protocol.py +1 -1
  43. oscura/export/legacy/__init__.py +11 -0
  44. oscura/export/legacy/wav.py +75 -0
  45. oscura/exporters/__init__.py +19 -0
  46. oscura/exporters/wireshark.py +809 -0
  47. oscura/hardware/acquisition/file.py +5 -19
  48. oscura/hardware/acquisition/saleae.py +10 -10
  49. oscura/hardware/acquisition/socketcan.py +4 -6
  50. oscura/hardware/acquisition/synthetic.py +1 -5
  51. oscura/hardware/acquisition/visa.py +6 -6
  52. oscura/hardware/security/side_channel_detector.py +5 -508
  53. oscura/inference/message_format.py +686 -1
  54. oscura/jupyter/display.py +2 -2
  55. oscura/jupyter/magic.py +3 -3
  56. oscura/loaders/__init__.py +17 -12
  57. oscura/loaders/binary.py +1 -1
  58. oscura/loaders/chipwhisperer.py +1 -2
  59. oscura/loaders/configurable.py +1 -1
  60. oscura/loaders/csv_loader.py +2 -2
  61. oscura/loaders/hdf5_loader.py +1 -1
  62. oscura/loaders/lazy.py +6 -1
  63. oscura/loaders/mmap_loader.py +0 -1
  64. oscura/loaders/numpy_loader.py +8 -7
  65. oscura/loaders/preprocessing.py +3 -5
  66. oscura/loaders/rigol.py +21 -7
  67. oscura/loaders/sigrok.py +2 -5
  68. oscura/loaders/tdms.py +3 -2
  69. oscura/loaders/tektronix.py +38 -32
  70. oscura/loaders/tss.py +20 -27
  71. oscura/loaders/vcd.py +13 -8
  72. oscura/loaders/wav.py +1 -6
  73. oscura/pipeline/__init__.py +76 -0
  74. oscura/pipeline/handlers/__init__.py +165 -0
  75. oscura/pipeline/handlers/analyzers.py +1045 -0
  76. oscura/pipeline/handlers/decoders.py +899 -0
  77. oscura/pipeline/handlers/exporters.py +1103 -0
  78. oscura/pipeline/handlers/filters.py +891 -0
  79. oscura/pipeline/handlers/loaders.py +640 -0
  80. oscura/pipeline/handlers/transforms.py +768 -0
  81. oscura/reporting/formatting/measurements.py +55 -14
  82. oscura/reporting/templates/enhanced/protocol_re.html +504 -503
  83. oscura/side_channel/__init__.py +38 -57
  84. oscura/utils/builders/signal_builder.py +5 -5
  85. oscura/utils/comparison/compare.py +7 -9
  86. oscura/utils/comparison/golden.py +1 -1
  87. oscura/utils/filtering/convenience.py +2 -2
  88. oscura/utils/math/arithmetic.py +38 -62
  89. oscura/utils/math/interpolation.py +20 -20
  90. oscura/utils/pipeline/__init__.py +4 -17
  91. oscura/utils/progressive.py +1 -4
  92. oscura/utils/triggering/edge.py +1 -1
  93. oscura/utils/triggering/pattern.py +2 -2
  94. oscura/utils/triggering/pulse.py +2 -2
  95. oscura/utils/triggering/window.py +3 -3
  96. oscura/validation/hil_testing.py +11 -11
  97. oscura/visualization/__init__.py +46 -284
  98. oscura/visualization/batch.py +72 -433
  99. oscura/visualization/plot.py +542 -53
  100. oscura/visualization/styles.py +184 -318
  101. oscura/workflows/batch/advanced.py +1 -1
  102. oscura/workflows/batch/aggregate.py +7 -8
  103. oscura/workflows/complete_re.py +251 -23
  104. oscura/workflows/digital.py +27 -4
  105. oscura/workflows/multi_trace.py +136 -17
  106. oscura/workflows/waveform.py +11 -6
  107. {oscura-0.8.0.dist-info → oscura-0.10.0.dist-info}/METADATA +59 -79
  108. {oscura-0.8.0.dist-info → oscura-0.10.0.dist-info}/RECORD +111 -136
  109. oscura/side_channel/dpa.py +0 -1025
  110. oscura/utils/optimization/__init__.py +0 -19
  111. oscura/utils/optimization/parallel.py +0 -443
  112. oscura/utils/optimization/search.py +0 -532
  113. oscura/utils/pipeline/base.py +0 -338
  114. oscura/utils/pipeline/composition.py +0 -248
  115. oscura/utils/pipeline/parallel.py +0 -449
  116. oscura/utils/pipeline/pipeline.py +0 -375
  117. oscura/utils/search/__init__.py +0 -16
  118. oscura/utils/search/anomaly.py +0 -424
  119. oscura/utils/search/context.py +0 -294
  120. oscura/utils/search/pattern.py +0 -288
  121. oscura/utils/storage/__init__.py +0 -61
  122. oscura/utils/storage/database.py +0 -1166
  123. oscura/visualization/accessibility.py +0 -526
  124. oscura/visualization/annotations.py +0 -371
  125. oscura/visualization/axis_scaling.py +0 -305
  126. oscura/visualization/colors.py +0 -451
  127. oscura/visualization/digital.py +0 -436
  128. oscura/visualization/eye.py +0 -571
  129. oscura/visualization/histogram.py +0 -281
  130. oscura/visualization/interactive.py +0 -1035
  131. oscura/visualization/jitter.py +0 -1042
  132. oscura/visualization/keyboard.py +0 -394
  133. oscura/visualization/layout.py +0 -400
  134. oscura/visualization/optimization.py +0 -1079
  135. oscura/visualization/palettes.py +0 -446
  136. oscura/visualization/power.py +0 -508
  137. oscura/visualization/power_extended.py +0 -955
  138. oscura/visualization/presets.py +0 -469
  139. oscura/visualization/protocols.py +0 -1246
  140. oscura/visualization/render.py +0 -223
  141. oscura/visualization/rendering.py +0 -444
  142. oscura/visualization/reverse_engineering.py +0 -838
  143. oscura/visualization/signal_integrity.py +0 -989
  144. oscura/visualization/specialized.py +0 -643
  145. oscura/visualization/spectral.py +0 -1226
  146. oscura/visualization/thumbnails.py +0 -340
  147. oscura/visualization/time_axis.py +0 -351
  148. oscura/visualization/waveform.py +0 -454
  149. {oscura-0.8.0.dist-info → oscura-0.10.0.dist-info}/WHEEL +0 -0
  150. {oscura-0.8.0.dist-info → oscura-0.10.0.dist-info}/entry_points.txt +0 -0
  151. {oscura-0.8.0.dist-info → oscura-0.10.0.dist-info}/licenses/LICENSE +0 -0
oscura/cli/pipeline.py ADDED
@@ -0,0 +1,585 @@
1
+ """Pipeline CLI command group for Oscura.
2
+
3
+ Provides command-line interface for pipeline operations including execution,
4
+ validation, and handler inspection.
5
+
6
+ Example:
7
+ $ oscura pipeline run analysis.yaml
8
+ $ oscura pipeline run analysis.yaml --var input_file=trace.wfm --var baud_rate=115200
9
+ $ oscura pipeline validate analysis.yaml
10
+ $ oscura pipeline handlers
11
+ $ oscura pipeline handler-info decoder.uart
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import json
17
+ import logging
18
+ import sys
19
+ from typing import TYPE_CHECKING, Any
20
+
21
+ import click
22
+
23
+ from oscura.pipeline import (
24
+ Pipeline,
25
+ PipelineExecutionError,
26
+ PipelineValidationError,
27
+ register_all_handlers,
28
+ )
29
+ from oscura.pipeline.handlers import get_all_handlers, get_handler, list_handler_types
30
+
31
+ if TYPE_CHECKING:
32
+ from collections.abc import Callable
33
+
34
+ logger = logging.getLogger("oscura.cli.pipeline")
35
+
36
+
37
+ def _parse_variables(var_tuples: tuple[str, ...]) -> dict[str, Any]:
38
+ """Parse --var key=value arguments into dictionary.
39
+
40
+ Args:
41
+ var_tuples: Tuple of "key=value" strings
42
+
43
+ Returns:
44
+ Dictionary of parsed variables
45
+
46
+ Raises:
47
+ click.BadParameter: If variable format is invalid
48
+
49
+ Example:
50
+ >>> _parse_variables(("input_file=trace.wfm", "baud_rate=115200"))
51
+ {"input_file": "trace.wfm", "baud_rate": 115200}
52
+ """
53
+ variables: dict[str, Any] = {}
54
+ for var_str in var_tuples:
55
+ if "=" not in var_str:
56
+ raise click.BadParameter(f"Invalid variable format: '{var_str}'. Expected 'key=value'.")
57
+
58
+ key, value = var_str.split("=", 1)
59
+ key = key.strip()
60
+ value = value.strip()
61
+
62
+ # Try to parse as int, float, bool, or keep as string
63
+ parsed_value: Any
64
+ if value.lower() in ("true", "yes", "on"):
65
+ parsed_value = True
66
+ elif value.lower() in ("false", "no", "off"):
67
+ parsed_value = False
68
+ elif value.isdigit():
69
+ parsed_value = int(value)
70
+ else:
71
+ try:
72
+ parsed_value = float(value)
73
+ except ValueError:
74
+ parsed_value = value
75
+
76
+ variables[key] = parsed_value
77
+
78
+ return variables
79
+
80
+
81
+ def _create_progress_callback(verbose: bool) -> Callable[[str, int], None]:
82
+ """Create progress callback for pipeline execution.
83
+
84
+ Args:
85
+ verbose: Whether to show detailed progress
86
+
87
+ Returns:
88
+ Progress callback function
89
+ """
90
+
91
+ def progress_callback(step_name: str, percent: int) -> None:
92
+ """Report pipeline execution progress.
93
+
94
+ Args:
95
+ step_name: Name of current step
96
+ percent: Completion percentage (0-100)
97
+ """
98
+ if verbose:
99
+ click.echo(f"[{percent:3d}%] Executing step: {step_name}")
100
+
101
+ return progress_callback
102
+
103
+
104
+ def _format_handler_info(handler_type: str, handler_func: Callable[..., Any]) -> dict[str, Any]:
105
+ """Extract handler information from function.
106
+
107
+ Args:
108
+ handler_type: Handler type identifier (e.g., "input.file")
109
+ handler_func: Handler function
110
+
111
+ Returns:
112
+ Dictionary with handler metadata
113
+ """
114
+ # Parse docstring to extract metadata
115
+ docstring = handler_func.__doc__ or ""
116
+ lines = docstring.strip().split("\n")
117
+
118
+ # Extract description (first line or paragraph)
119
+ description = ""
120
+ params_section = []
121
+ outputs_section = []
122
+ current_section = None
123
+
124
+ for line in lines:
125
+ line = line.strip()
126
+ if line.lower().startswith("parameters:"):
127
+ current_section = "params"
128
+ continue
129
+ elif line.lower().startswith("outputs:"):
130
+ current_section = "outputs"
131
+ continue
132
+ elif line.lower().startswith("inputs:"):
133
+ current_section = "inputs"
134
+ continue
135
+
136
+ if current_section == "params" and line:
137
+ params_section.append(line)
138
+ elif current_section == "outputs" and line:
139
+ outputs_section.append(line)
140
+ elif not description and line and current_section is None:
141
+ description = line
142
+
143
+ # Determine category from handler type
144
+ category = handler_type.split(".")[0] if "." in handler_type else "unknown"
145
+
146
+ result: dict[str, Any] = {
147
+ "type": handler_type,
148
+ "category": category,
149
+ "description": description,
150
+ "parameters": params_section,
151
+ "outputs": outputs_section,
152
+ }
153
+ return result
154
+
155
+
156
+ @click.group()
157
+ def pipeline() -> None:
158
+ """Pipeline operations for declarative workflows.
159
+
160
+ Execute, validate, and inspect YAML-based analysis pipelines with
161
+ 60+ built-in handlers for common operations.
162
+
163
+ Example:
164
+ oscura pipeline run analysis.yaml
165
+ oscura pipeline validate analysis.yaml
166
+ oscura pipeline handlers
167
+ """
168
+
169
+
170
+ @pipeline.command()
171
+ @click.argument("yaml_file", type=click.Path(exists=True))
172
+ @click.option(
173
+ "--var",
174
+ multiple=True,
175
+ help="Set template variable (format: key=value). Can be used multiple times.",
176
+ )
177
+ @click.option(
178
+ "--dry-run",
179
+ is_flag=True,
180
+ help="Validate pipeline without executing.",
181
+ )
182
+ @click.option(
183
+ "--json",
184
+ "json_output",
185
+ is_flag=True,
186
+ help="Output results as JSON.",
187
+ )
188
+ @click.option(
189
+ "--verbose",
190
+ "-v",
191
+ is_flag=True,
192
+ help="Enable verbose logging and progress reporting.",
193
+ )
194
+ @click.pass_context
195
+ def run(
196
+ ctx: click.Context,
197
+ yaml_file: str,
198
+ var: tuple[str, ...],
199
+ dry_run: bool,
200
+ json_output: bool,
201
+ verbose: bool,
202
+ ) -> None:
203
+ """Execute a pipeline from YAML file.
204
+
205
+ Loads and executes the specified pipeline with optional template variables.
206
+ Displays progress during execution and reports results.
207
+
208
+ Args:
209
+ ctx: Click context object.
210
+ yaml_file: Path to pipeline YAML file.
211
+ var: Template variables (key=value pairs).
212
+ dry_run: If True, validate only without executing.
213
+ json_output: If True, output results as JSON.
214
+ verbose: If True, enable detailed progress reporting.
215
+
216
+ Example:
217
+ oscura pipeline run analysis.yaml
218
+ oscura pipeline run analysis.yaml --var input_file=trace.wfm --var baud_rate=115200
219
+ oscura pipeline run analysis.yaml --dry-run
220
+ oscura pipeline run analysis.yaml --json
221
+ """
222
+ # Set logging level
223
+ if verbose:
224
+ logging.basicConfig(level=logging.INFO)
225
+ logger.setLevel(logging.INFO)
226
+
227
+ # Parse variables
228
+ variables = _parse_variables(var)
229
+
230
+ try:
231
+ # Load pipeline
232
+ if verbose:
233
+ click.echo(f"Loading pipeline: {yaml_file}")
234
+ pipeline_obj = Pipeline.load(yaml_file, variables=variables)
235
+
236
+ # Register all handlers
237
+ register_all_handlers(pipeline_obj)
238
+
239
+ # Dry-run mode: validate only
240
+ if dry_run:
241
+ if verbose:
242
+ click.echo("Validating pipeline (dry-run mode)...")
243
+ # Validation happens during load
244
+ if json_output:
245
+ result = {
246
+ "status": "valid",
247
+ "pipeline": pipeline_obj.definition.name,
248
+ "steps": len(pipeline_obj.definition.steps),
249
+ }
250
+ click.echo(json.dumps(result, indent=2))
251
+ else:
252
+ click.echo(f"Pipeline '{pipeline_obj.definition.name}' is valid.")
253
+ click.echo(f"Steps: {len(pipeline_obj.definition.steps)}")
254
+ return
255
+
256
+ # Execute pipeline
257
+ if verbose:
258
+ click.echo(f"Executing pipeline: {pipeline_obj.definition.name}")
259
+ pipeline_obj.on_progress(_create_progress_callback(verbose))
260
+
261
+ results = pipeline_obj.execute()
262
+
263
+ # Display results
264
+ if json_output:
265
+ output = {
266
+ "status": "success" if results.success else "error",
267
+ "pipeline": results.pipeline_name,
268
+ "steps_executed": len(results.step_results),
269
+ "outputs": {
270
+ key: str(value)[:100]
271
+ if not isinstance(value, (int, float, bool, str))
272
+ else value
273
+ for key, value in results.outputs.items()
274
+ },
275
+ }
276
+ if results.error:
277
+ output["error"] = results.error
278
+ click.echo(json.dumps(output, indent=2))
279
+ else:
280
+ click.echo(f"\nPipeline '{results.pipeline_name}' completed successfully.")
281
+ click.echo(f"Steps executed: {len(results.step_results)}")
282
+ if results.outputs:
283
+ click.echo("\nOutputs:")
284
+ for key, value in results.outputs.items():
285
+ value_str = str(value)
286
+ if len(value_str) > 80:
287
+ value_str = value_str[:77] + "..."
288
+ click.echo(f" {key}: {value_str}")
289
+
290
+ except PipelineValidationError as e:
291
+ if json_output:
292
+ error: dict[str, Any] = {
293
+ "status": "validation_error",
294
+ "message": str(e),
295
+ }
296
+ if e.step_name is not None:
297
+ error["step"] = e.step_name
298
+ if e.suggestion is not None:
299
+ error["suggestion"] = e.suggestion
300
+ click.echo(json.dumps(error, indent=2))
301
+ else:
302
+ click.echo(f"Validation error: {e}", err=True)
303
+ if e.step_name:
304
+ click.echo(f" Step: {e.step_name}", err=True)
305
+ if e.suggestion:
306
+ click.echo(f" Suggestion: {e.suggestion}", err=True)
307
+ sys.exit(1)
308
+
309
+ except PipelineExecutionError as e:
310
+ if json_output:
311
+ error = {
312
+ "status": "execution_error",
313
+ "message": str(e),
314
+ "step": e.step_name,
315
+ }
316
+ if e.traceback_str:
317
+ error["traceback"] = e.traceback_str
318
+ click.echo(json.dumps(error, indent=2))
319
+ else:
320
+ click.echo(f"Execution error: {e}", err=True)
321
+ if e.step_name:
322
+ click.echo(f" Step: {e.step_name}", err=True)
323
+ if e.traceback_str and verbose:
324
+ click.echo(f"\nTraceback:\n{e.traceback_str}", err=True)
325
+ sys.exit(1)
326
+
327
+ except FileNotFoundError as e:
328
+ if json_output:
329
+ error = {"status": "file_not_found", "message": str(e)}
330
+ click.echo(json.dumps(error, indent=2))
331
+ else:
332
+ click.echo(f"File not found: {e}", err=True)
333
+ sys.exit(1)
334
+
335
+ except Exception as e:
336
+ if json_output:
337
+ error = {"status": "error", "message": str(e), "type": type(e).__name__}
338
+ click.echo(json.dumps(error, indent=2))
339
+ else:
340
+ click.echo(f"Unexpected error: {e}", err=True)
341
+ if verbose:
342
+ import traceback
343
+
344
+ click.echo(traceback.format_exc(), err=True)
345
+ sys.exit(1)
346
+
347
+
348
+ @pipeline.command()
349
+ @click.argument("yaml_file", type=click.Path(exists=True))
350
+ @click.option(
351
+ "--var",
352
+ multiple=True,
353
+ help="Set template variable (format: key=value). Can be used multiple times.",
354
+ )
355
+ @click.option(
356
+ "--json",
357
+ "json_output",
358
+ is_flag=True,
359
+ help="Output results as JSON.",
360
+ )
361
+ def validate(
362
+ yaml_file: str,
363
+ var: tuple[str, ...],
364
+ json_output: bool,
365
+ ) -> None:
366
+ """Validate pipeline without executing (dry-run).
367
+
368
+ Checks pipeline syntax, step dependencies, and handler availability
369
+ without executing any steps.
370
+
371
+ Args:
372
+ yaml_file: Path to pipeline YAML file.
373
+ var: Template variables (key=value pairs).
374
+ json_output: If True, output results as JSON.
375
+
376
+ Example:
377
+ oscura pipeline validate analysis.yaml
378
+ oscura pipeline validate analysis.yaml --var input_file=trace.wfm
379
+ oscura pipeline validate analysis.yaml --json
380
+ """
381
+ # Parse variables
382
+ variables = _parse_variables(var)
383
+
384
+ try:
385
+ # Load pipeline (validation happens during load)
386
+ pipeline_obj = Pipeline.load(yaml_file, variables=variables)
387
+
388
+ # Register handlers to check availability
389
+ register_all_handlers(pipeline_obj)
390
+
391
+ # Check all steps have handlers
392
+ missing_handlers = []
393
+ for step in pipeline_obj.definition.steps:
394
+ if get_handler(step.type) is None:
395
+ missing_handlers.append(step.type)
396
+
397
+ if missing_handlers:
398
+ if json_output:
399
+ error_data: dict[str, Any] = {
400
+ "status": "validation_error",
401
+ "message": f"Missing handlers for step types: {', '.join(missing_handlers)}",
402
+ "missing_handlers": missing_handlers,
403
+ }
404
+ click.echo(json.dumps(error_data, indent=2))
405
+ else:
406
+ click.echo("Validation error: Missing handlers", err=True)
407
+ click.echo(f" Missing: {', '.join(missing_handlers)}", err=True)
408
+ click.echo("\nRun 'oscura pipeline handlers' to see available handlers.", err=True)
409
+ sys.exit(1)
410
+
411
+ # Success
412
+ if json_output:
413
+ result = {
414
+ "status": "valid",
415
+ "pipeline": pipeline_obj.definition.name,
416
+ "version": pipeline_obj.definition.version,
417
+ "steps": len(pipeline_obj.definition.steps),
418
+ "description": pipeline_obj.definition.description,
419
+ }
420
+ click.echo(json.dumps(result, indent=2))
421
+ else:
422
+ click.echo(f"Pipeline '{pipeline_obj.definition.name}' is valid.")
423
+ click.echo(f" Version: {pipeline_obj.definition.version}")
424
+ click.echo(f" Steps: {len(pipeline_obj.definition.steps)}")
425
+ if pipeline_obj.definition.description:
426
+ click.echo(f" Description: {pipeline_obj.definition.description}")
427
+
428
+ except PipelineValidationError as e:
429
+ if json_output:
430
+ error_info: dict[str, Any] = {
431
+ "status": "validation_error",
432
+ "message": str(e),
433
+ }
434
+ if e.step_name is not None:
435
+ error_info["step"] = e.step_name
436
+ if e.suggestion is not None:
437
+ error_info["suggestion"] = e.suggestion
438
+ click.echo(json.dumps(error_info, indent=2))
439
+ else:
440
+ click.echo(f"Validation error: {e}", err=True)
441
+ if e.step_name:
442
+ click.echo(f" Step: {e.step_name}", err=True)
443
+ if e.suggestion:
444
+ click.echo(f" Suggestion: {e.suggestion}", err=True)
445
+ sys.exit(1)
446
+
447
+ except FileNotFoundError as e:
448
+ if json_output:
449
+ error_msg: dict[str, Any] = {"status": "file_not_found", "message": str(e)}
450
+ click.echo(json.dumps(error_msg, indent=2))
451
+ else:
452
+ click.echo(f"File not found: {e}", err=True)
453
+ sys.exit(1)
454
+
455
+ except Exception as e:
456
+ if json_output:
457
+ error_obj: dict[str, Any] = {
458
+ "status": "error",
459
+ "message": str(e),
460
+ "type": type(e).__name__,
461
+ }
462
+ click.echo(json.dumps(error_obj, indent=2))
463
+ else:
464
+ click.echo(f"Unexpected error: {e}", err=True)
465
+ sys.exit(1)
466
+
467
+
468
+ @pipeline.command()
469
+ @click.option(
470
+ "--json",
471
+ "json_output",
472
+ is_flag=True,
473
+ help="Output results as JSON.",
474
+ )
475
+ def handlers(json_output: bool) -> None:
476
+ """List all registered pipeline handlers.
477
+
478
+ Displays all available handler types organized by category
479
+ (input, decoder, analysis, filter, transform, output).
480
+
481
+ Args:
482
+ json_output: If True, output results as JSON.
483
+
484
+ Example:
485
+ oscura pipeline handlers
486
+ oscura pipeline handlers --json
487
+ """
488
+ # Get all handlers
489
+ all_handlers = get_all_handlers()
490
+
491
+ if not all_handlers:
492
+ if json_output:
493
+ click.echo(json.dumps({"handlers": [], "total": 0}))
494
+ else:
495
+ click.echo("No handlers registered.")
496
+ return
497
+
498
+ # Group by category
499
+ categories: dict[str, list[str]] = {}
500
+ for handler_type in sorted(all_handlers.keys()):
501
+ category = handler_type.split(".")[0] if "." in handler_type else "other"
502
+ if category not in categories:
503
+ categories[category] = []
504
+ categories[category].append(handler_type)
505
+
506
+ # Output results
507
+ if json_output:
508
+ result = {
509
+ "handlers": sorted(all_handlers.keys()),
510
+ "categories": categories,
511
+ "total": len(all_handlers),
512
+ "by_category": {cat: len(handlers) for cat, handlers in categories.items()},
513
+ }
514
+ click.echo(json.dumps(result, indent=2))
515
+ else:
516
+ click.echo(f"Registered Pipeline Handlers ({len(all_handlers)} total):\n")
517
+ for category in sorted(categories.keys()):
518
+ handlers = categories[category]
519
+ click.echo(f"{category.upper()} ({len(handlers)}):")
520
+ for handler_type in handlers:
521
+ click.echo(f" - {handler_type}")
522
+ click.echo()
523
+
524
+
525
+ @pipeline.command()
526
+ @click.argument("handler_type")
527
+ @click.option(
528
+ "--json",
529
+ "json_output",
530
+ is_flag=True,
531
+ help="Output results as JSON.",
532
+ )
533
+ def handler_info(handler_type: str, json_output: bool) -> None:
534
+ """Show detailed information about a handler.
535
+
536
+ Displays handler documentation, parameters, inputs, and outputs.
537
+
538
+ Args:
539
+ handler_type: Handler type identifier (e.g., "decoder.uart").
540
+ json_output: If True, output results as JSON.
541
+
542
+ Example:
543
+ oscura pipeline handler-info decoder.uart
544
+ oscura pipeline handler-info input.file --json
545
+ """
546
+ handler = get_handler(handler_type)
547
+
548
+ if handler is None:
549
+ if json_output:
550
+ error = {
551
+ "status": "not_found",
552
+ "message": f"Handler '{handler_type}' not found",
553
+ "available": list_handler_types(),
554
+ }
555
+ click.echo(json.dumps(error, indent=2))
556
+ else:
557
+ click.echo(f"Handler '{handler_type}' not found.", err=True)
558
+ click.echo("\nAvailable handlers:", err=True)
559
+ for h in list_handler_types()[:5]:
560
+ click.echo(f" - {h}", err=True)
561
+ if len(list_handler_types()) > 5:
562
+ click.echo(f" ... and {len(list_handler_types()) - 5} more", err=True)
563
+ click.echo("\nRun 'oscura pipeline handlers' for full list.", err=True)
564
+ sys.exit(1)
565
+
566
+ # Extract handler information
567
+ info = _format_handler_info(handler_type, handler)
568
+
569
+ if json_output:
570
+ click.echo(json.dumps(info, indent=2))
571
+ else:
572
+ click.echo(f"Handler: {info['type']}")
573
+ click.echo(f"Category: {info['category']}")
574
+ if info["description"]:
575
+ click.echo(f"\nDescription:\n {info['description']}")
576
+
577
+ if info["parameters"]:
578
+ click.echo("\nParameters:")
579
+ for param in info["parameters"]:
580
+ click.echo(f" {param}")
581
+
582
+ if info["outputs"]:
583
+ click.echo("\nOutputs:")
584
+ for output in info["outputs"]:
585
+ click.echo(f" {output}")
oscura/cli/visualize.py CHANGED
@@ -90,10 +90,12 @@ def visualize(
90
90
 
91
91
  # Plot waveform (handle IQTrace separately)
92
92
  if isinstance(trace, IQTrace):
93
- # Plot I/Q components
94
- time_axis = np.arange(len(trace.i_data)) / trace.metadata.sample_rate
95
- ax.plot(time_axis * 1e3, trace.i_data, linewidth=0.5, label="I")
96
- ax.plot(time_axis * 1e3, trace.q_data, linewidth=0.5, label="Q")
93
+ # Plot I/Q components (extract from complex data)
94
+ i_data = trace.data.real
95
+ q_data = trace.data.imag
96
+ time_axis = np.arange(len(trace.data)) / trace.metadata.sample_rate
97
+ ax.plot(time_axis * 1e3, i_data, linewidth=0.5, label="I")
98
+ ax.plot(time_axis * 1e3, q_data, linewidth=0.5, label="Q")
97
99
  ax.legend()
98
100
  else:
99
101
  # Plot regular waveform or digital trace