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.
- oscura/__init__.py +19 -19
- oscura/analyzers/__init__.py +2 -0
- oscura/analyzers/digital/extraction.py +2 -3
- oscura/analyzers/digital/quality.py +1 -1
- oscura/analyzers/digital/timing.py +1 -1
- oscura/analyzers/patterns/__init__.py +66 -0
- oscura/analyzers/power/basic.py +3 -3
- oscura/analyzers/power/soa.py +1 -1
- oscura/analyzers/power/switching.py +3 -3
- oscura/analyzers/signal_classification.py +529 -0
- oscura/analyzers/signal_integrity/sparams.py +3 -3
- oscura/analyzers/statistics/basic.py +10 -7
- oscura/analyzers/validation.py +1 -1
- oscura/analyzers/waveform/measurements.py +200 -156
- oscura/analyzers/waveform/measurements_with_uncertainty.py +91 -35
- oscura/analyzers/waveform/spectral.py +164 -73
- oscura/api/dsl/commands.py +15 -6
- oscura/api/server/templates/base.html +137 -146
- oscura/api/server/templates/export.html +84 -110
- oscura/api/server/templates/home.html +248 -267
- oscura/api/server/templates/protocols.html +44 -48
- oscura/api/server/templates/reports.html +27 -35
- oscura/api/server/templates/session_detail.html +68 -78
- oscura/api/server/templates/sessions.html +62 -72
- oscura/api/server/templates/waveforms.html +54 -64
- oscura/automotive/__init__.py +1 -1
- oscura/automotive/can/session.py +1 -1
- oscura/automotive/dbc/generator.py +638 -23
- oscura/automotive/uds/decoder.py +99 -6
- oscura/cli/analyze.py +8 -2
- oscura/cli/batch.py +36 -5
- oscura/cli/characterize.py +18 -4
- oscura/cli/export.py +47 -5
- oscura/cli/main.py +2 -0
- oscura/cli/onboarding/wizard.py +10 -6
- oscura/cli/pipeline.py +585 -0
- oscura/cli/visualize.py +6 -4
- oscura/convenience.py +400 -32
- oscura/core/measurement_result.py +286 -0
- oscura/core/progress.py +1 -1
- oscura/core/types.py +232 -239
- oscura/correlation/multi_protocol.py +1 -1
- oscura/export/legacy/__init__.py +11 -0
- oscura/export/legacy/wav.py +75 -0
- oscura/exporters/__init__.py +19 -0
- oscura/exporters/wireshark.py +809 -0
- oscura/hardware/acquisition/file.py +5 -19
- oscura/hardware/acquisition/saleae.py +10 -10
- oscura/hardware/acquisition/socketcan.py +4 -6
- oscura/hardware/acquisition/synthetic.py +1 -5
- oscura/hardware/acquisition/visa.py +6 -6
- oscura/hardware/security/side_channel_detector.py +5 -508
- oscura/inference/message_format.py +686 -1
- oscura/jupyter/display.py +2 -2
- oscura/jupyter/magic.py +3 -3
- oscura/loaders/__init__.py +17 -12
- oscura/loaders/binary.py +1 -1
- oscura/loaders/chipwhisperer.py +1 -2
- oscura/loaders/configurable.py +1 -1
- oscura/loaders/csv_loader.py +2 -2
- oscura/loaders/hdf5_loader.py +1 -1
- oscura/loaders/lazy.py +6 -1
- oscura/loaders/mmap_loader.py +0 -1
- oscura/loaders/numpy_loader.py +8 -7
- oscura/loaders/preprocessing.py +3 -5
- oscura/loaders/rigol.py +21 -7
- oscura/loaders/sigrok.py +2 -5
- oscura/loaders/tdms.py +3 -2
- oscura/loaders/tektronix.py +38 -32
- oscura/loaders/tss.py +20 -27
- oscura/loaders/vcd.py +13 -8
- oscura/loaders/wav.py +1 -6
- oscura/pipeline/__init__.py +76 -0
- oscura/pipeline/handlers/__init__.py +165 -0
- oscura/pipeline/handlers/analyzers.py +1045 -0
- oscura/pipeline/handlers/decoders.py +899 -0
- oscura/pipeline/handlers/exporters.py +1103 -0
- oscura/pipeline/handlers/filters.py +891 -0
- oscura/pipeline/handlers/loaders.py +640 -0
- oscura/pipeline/handlers/transforms.py +768 -0
- oscura/reporting/formatting/measurements.py +55 -14
- oscura/reporting/templates/enhanced/protocol_re.html +504 -503
- oscura/side_channel/__init__.py +38 -57
- oscura/utils/builders/signal_builder.py +5 -5
- oscura/utils/comparison/compare.py +7 -9
- oscura/utils/comparison/golden.py +1 -1
- oscura/utils/filtering/convenience.py +2 -2
- oscura/utils/math/arithmetic.py +38 -62
- oscura/utils/math/interpolation.py +20 -20
- oscura/utils/pipeline/__init__.py +4 -17
- oscura/utils/progressive.py +1 -4
- oscura/utils/triggering/edge.py +1 -1
- oscura/utils/triggering/pattern.py +2 -2
- oscura/utils/triggering/pulse.py +2 -2
- oscura/utils/triggering/window.py +3 -3
- oscura/validation/hil_testing.py +11 -11
- oscura/visualization/__init__.py +46 -284
- oscura/visualization/batch.py +72 -433
- oscura/visualization/plot.py +542 -53
- oscura/visualization/styles.py +184 -318
- oscura/workflows/batch/advanced.py +1 -1
- oscura/workflows/batch/aggregate.py +7 -8
- oscura/workflows/complete_re.py +251 -23
- oscura/workflows/digital.py +27 -4
- oscura/workflows/multi_trace.py +136 -17
- oscura/workflows/waveform.py +11 -6
- {oscura-0.8.0.dist-info → oscura-0.10.0.dist-info}/METADATA +59 -79
- {oscura-0.8.0.dist-info → oscura-0.10.0.dist-info}/RECORD +111 -136
- oscura/side_channel/dpa.py +0 -1025
- oscura/utils/optimization/__init__.py +0 -19
- oscura/utils/optimization/parallel.py +0 -443
- oscura/utils/optimization/search.py +0 -532
- oscura/utils/pipeline/base.py +0 -338
- oscura/utils/pipeline/composition.py +0 -248
- oscura/utils/pipeline/parallel.py +0 -449
- oscura/utils/pipeline/pipeline.py +0 -375
- oscura/utils/search/__init__.py +0 -16
- oscura/utils/search/anomaly.py +0 -424
- oscura/utils/search/context.py +0 -294
- oscura/utils/search/pattern.py +0 -288
- oscura/utils/storage/__init__.py +0 -61
- oscura/utils/storage/database.py +0 -1166
- oscura/visualization/accessibility.py +0 -526
- oscura/visualization/annotations.py +0 -371
- oscura/visualization/axis_scaling.py +0 -305
- oscura/visualization/colors.py +0 -451
- oscura/visualization/digital.py +0 -436
- oscura/visualization/eye.py +0 -571
- oscura/visualization/histogram.py +0 -281
- oscura/visualization/interactive.py +0 -1035
- oscura/visualization/jitter.py +0 -1042
- oscura/visualization/keyboard.py +0 -394
- oscura/visualization/layout.py +0 -400
- oscura/visualization/optimization.py +0 -1079
- oscura/visualization/palettes.py +0 -446
- oscura/visualization/power.py +0 -508
- oscura/visualization/power_extended.py +0 -955
- oscura/visualization/presets.py +0 -469
- oscura/visualization/protocols.py +0 -1246
- oscura/visualization/render.py +0 -223
- oscura/visualization/rendering.py +0 -444
- oscura/visualization/reverse_engineering.py +0 -838
- oscura/visualization/signal_integrity.py +0 -989
- oscura/visualization/specialized.py +0 -643
- oscura/visualization/spectral.py +0 -1226
- oscura/visualization/thumbnails.py +0 -340
- oscura/visualization/time_axis.py +0 -351
- oscura/visualization/waveform.py +0 -454
- {oscura-0.8.0.dist-info → oscura-0.10.0.dist-info}/WHEEL +0 -0
- {oscura-0.8.0.dist-info → oscura-0.10.0.dist-info}/entry_points.txt +0 -0
- {oscura-0.8.0.dist-info → oscura-0.10.0.dist-info}/licenses/LICENSE +0 -0
oscura/automotive/uds/decoder.py
CHANGED
|
@@ -5,6 +5,7 @@ This module implements decoding for UDS diagnostic messages used in automotive E
|
|
|
5
5
|
|
|
6
6
|
from __future__ import annotations
|
|
7
7
|
|
|
8
|
+
from dataclasses import dataclass
|
|
8
9
|
from typing import TYPE_CHECKING
|
|
9
10
|
|
|
10
11
|
if TYPE_CHECKING:
|
|
@@ -12,7 +13,26 @@ if TYPE_CHECKING:
|
|
|
12
13
|
|
|
13
14
|
from oscura.automotive.uds.models import UDSNegativeResponse, UDSService
|
|
14
15
|
|
|
15
|
-
__all__ = ["UDSDecoder"]
|
|
16
|
+
__all__ = ["UDSDecoder", "UDSResponse"]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class UDSResponse:
|
|
21
|
+
"""A decoded UDS response message.
|
|
22
|
+
|
|
23
|
+
Attributes:
|
|
24
|
+
service: Service ID.
|
|
25
|
+
data: Response data payload.
|
|
26
|
+
timestamp: Message timestamp.
|
|
27
|
+
is_negative: True if negative response (NRC).
|
|
28
|
+
nrc: Negative Response Code (if is_negative=True).
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
service: int
|
|
32
|
+
data: bytes
|
|
33
|
+
timestamp: float
|
|
34
|
+
is_negative: bool = False
|
|
35
|
+
nrc: int | None = None
|
|
16
36
|
|
|
17
37
|
|
|
18
38
|
# Service ID mappings per ISO 14229-1
|
|
@@ -146,6 +166,58 @@ class UDSDecoder:
|
|
|
146
166
|
|
|
147
167
|
return False
|
|
148
168
|
|
|
169
|
+
@staticmethod
|
|
170
|
+
def decode(message: CANMessage) -> UDSResponse | None:
|
|
171
|
+
"""Decode UDS message from CAN message (returns UDSResponse).
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
message: CAN message to decode.
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
UDSResponse with decoded information, or None if not a valid UDS message.
|
|
178
|
+
|
|
179
|
+
Example:
|
|
180
|
+
>>> msg = CANMessage(id=0x7E0, timestamp=1.0, data=bytes([0x02, 0x10, 0x01]))
|
|
181
|
+
>>> response = UDSDecoder.decode(msg)
|
|
182
|
+
>>> print(f"Service: 0x{response.service:02X}")
|
|
183
|
+
"""
|
|
184
|
+
if len(message.data) < 2:
|
|
185
|
+
return None
|
|
186
|
+
|
|
187
|
+
# Extract UDS payload from ISO-TP frame if needed
|
|
188
|
+
data = UDSDecoder._extract_uds_payload(message.data)
|
|
189
|
+
if not data:
|
|
190
|
+
return None
|
|
191
|
+
|
|
192
|
+
# Check for negative response
|
|
193
|
+
if data[0] == 0x7F:
|
|
194
|
+
neg_resp = UDSDecoder._decode_negative_response(data)
|
|
195
|
+
if neg_resp is None:
|
|
196
|
+
return None
|
|
197
|
+
return UDSResponse(
|
|
198
|
+
service=0x7F,
|
|
199
|
+
data=data,
|
|
200
|
+
timestamp=message.timestamp,
|
|
201
|
+
is_negative=True,
|
|
202
|
+
nrc=neg_resp.nrc,
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
# Determine SID and request/response type
|
|
206
|
+
sid_info = UDSDecoder._parse_sid_byte(data[0])
|
|
207
|
+
if sid_info is None:
|
|
208
|
+
return None
|
|
209
|
+
|
|
210
|
+
sid, canonical_sid, is_request = sid_info
|
|
211
|
+
|
|
212
|
+
# Return UDSResponse
|
|
213
|
+
return UDSResponse(
|
|
214
|
+
service=sid,
|
|
215
|
+
data=data,
|
|
216
|
+
timestamp=message.timestamp,
|
|
217
|
+
is_negative=False,
|
|
218
|
+
nrc=None,
|
|
219
|
+
)
|
|
220
|
+
|
|
149
221
|
@staticmethod
|
|
150
222
|
def decode_service(message: CANMessage) -> UDSService | UDSNegativeResponse | None:
|
|
151
223
|
"""Decode UDS service from CAN message.
|
|
@@ -198,7 +270,11 @@ class UDSDecoder:
|
|
|
198
270
|
def _extract_uds_payload(message_data: bytes) -> bytes:
|
|
199
271
|
"""Extract UDS payload from CAN message data.
|
|
200
272
|
|
|
201
|
-
Handles ISO-TP
|
|
273
|
+
Handles ISO-TP frame formats:
|
|
274
|
+
- Single frame: 0x0X (X = length)
|
|
275
|
+
- First frame: 0x1X (X = upper nibble of length)
|
|
276
|
+
- Consecutive frame: 0x2X (X = sequence number)
|
|
277
|
+
- Flow control: 0x3X
|
|
202
278
|
|
|
203
279
|
Args:
|
|
204
280
|
message_data: Raw CAN message data.
|
|
@@ -206,14 +282,31 @@ class UDSDecoder:
|
|
|
206
282
|
Returns:
|
|
207
283
|
UDS payload bytes (empty if invalid).
|
|
208
284
|
"""
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
285
|
+
pci = message_data[0]
|
|
286
|
+
pci_type = (pci >> 4) & 0x0F
|
|
287
|
+
|
|
288
|
+
if pci_type == 0:
|
|
289
|
+
# ISO-TP single frame: [0x0X, ...UDS data...]
|
|
290
|
+
uds_length = pci & 0x0F
|
|
291
|
+
if len(message_data) < 1 + uds_length:
|
|
292
|
+
return b""
|
|
293
|
+
return message_data[1 : 1 + uds_length]
|
|
294
|
+
elif pci_type == 1:
|
|
295
|
+
# ISO-TP first frame: [0x1X, length_low, ...UDS data...]
|
|
296
|
+
# Skip PCI bytes (2 bytes) and return remaining as UDS payload
|
|
297
|
+
return message_data[2:]
|
|
298
|
+
elif pci_type == 2:
|
|
299
|
+
# ISO-TP consecutive frame: [0x2X, ...data...]
|
|
300
|
+
# Skip PCI byte and return data (but this is continuation, not standalone UDS)
|
|
301
|
+
return message_data[1:]
|
|
302
|
+
elif pci <= 0x07:
|
|
303
|
+
# Legacy single frame format (for compatibility)
|
|
304
|
+
uds_length = pci
|
|
212
305
|
if len(message_data) < 1 + uds_length:
|
|
213
306
|
return b""
|
|
214
307
|
return message_data[1 : 1 + uds_length]
|
|
215
308
|
else:
|
|
216
|
-
# Direct UDS: all bytes
|
|
309
|
+
# Direct UDS or unknown format: treat all bytes as UDS data
|
|
217
310
|
return message_data
|
|
218
311
|
|
|
219
312
|
@staticmethod
|
oscura/cli/analyze.py
CHANGED
|
@@ -222,14 +222,20 @@ def _characterize_signal(trace: Any) -> dict[str, Any]:
|
|
|
222
222
|
|
|
223
223
|
rt = rise_time(trace)
|
|
224
224
|
ft = fall_time(trace)
|
|
225
|
+
rt_val = rt["value"] if rt["applicable"] else None
|
|
226
|
+
ft_val = ft["value"] if ft["applicable"] else None
|
|
225
227
|
|
|
226
228
|
return {
|
|
227
229
|
"sample_rate": f"{sample_rate / 1e6:.1f} MHz",
|
|
228
230
|
"samples": len(data),
|
|
229
231
|
"duration": f"{len(data) / sample_rate * 1e3:.3f} ms",
|
|
230
232
|
"amplitude": f"{float(data.max() - data.min()):.3f} V",
|
|
231
|
-
"rise_time": f"{
|
|
232
|
-
|
|
233
|
+
"rise_time": f"{rt_val * 1e9:.2f} ns"
|
|
234
|
+
if rt_val is not None and not np.isnan(rt_val)
|
|
235
|
+
else "N/A",
|
|
236
|
+
"fall_time": f"{ft_val * 1e9:.2f} ns"
|
|
237
|
+
if ft_val is not None and not np.isnan(ft_val)
|
|
238
|
+
else "N/A",
|
|
233
239
|
}
|
|
234
240
|
|
|
235
241
|
|
oscura/cli/batch.py
CHANGED
|
@@ -169,17 +169,37 @@ def _analyze_single_file(file_path: str, analysis_type: str) -> dict[str, Any]:
|
|
|
169
169
|
"file": str(Path(file_path).name),
|
|
170
170
|
"status": "success",
|
|
171
171
|
"analysis_type": analysis_type,
|
|
172
|
-
"samples": len(trace.data),
|
|
172
|
+
"samples": len(trace.data),
|
|
173
173
|
"sample_rate": f"{sample_rate / 1e6:.1f} MHz",
|
|
174
174
|
}
|
|
175
175
|
|
|
176
176
|
if analysis_type == "characterize":
|
|
177
177
|
rt = rise_time(trace) # type: ignore[arg-type]
|
|
178
178
|
ft = fall_time(trace) # type: ignore[arg-type]
|
|
179
|
+
|
|
180
|
+
# Handle both MeasurementResult dict and raw float/nan values (for test compatibility)
|
|
181
|
+
if isinstance(rt, dict):
|
|
182
|
+
rt_val = rt["value"] if rt.get("applicable", True) else None
|
|
183
|
+
elif isinstance(rt, (int, float)): # type: ignore[unreachable]
|
|
184
|
+
rt_val = rt
|
|
185
|
+
else:
|
|
186
|
+
rt_val = None
|
|
187
|
+
|
|
188
|
+
if isinstance(ft, dict):
|
|
189
|
+
ft_val = ft["value"] if ft.get("applicable", True) else None
|
|
190
|
+
elif isinstance(ft, (int, float)): # type: ignore[unreachable]
|
|
191
|
+
ft_val = ft
|
|
192
|
+
else:
|
|
193
|
+
ft_val = None
|
|
194
|
+
|
|
179
195
|
result.update(
|
|
180
196
|
{
|
|
181
|
-
"rise_time": f"{
|
|
182
|
-
|
|
197
|
+
"rise_time": f"{rt_val * 1e9:.2f} ns"
|
|
198
|
+
if rt_val is not None and not np.isnan(rt_val)
|
|
199
|
+
else "N/A",
|
|
200
|
+
"fall_time": f"{ft_val * 1e9:.2f} ns"
|
|
201
|
+
if ft_val is not None and not np.isnan(ft_val)
|
|
202
|
+
else "N/A",
|
|
183
203
|
}
|
|
184
204
|
)
|
|
185
205
|
elif analysis_type == "decode":
|
|
@@ -197,11 +217,22 @@ def _analyze_single_file(file_path: str, analysis_type: str) -> dict[str, Any]:
|
|
|
197
217
|
peak_freq = freqs[peak_idx]
|
|
198
218
|
else:
|
|
199
219
|
peak_freq = 0.0
|
|
200
|
-
|
|
220
|
+
thd_result = thd(trace) # type: ignore[arg-type]
|
|
221
|
+
|
|
222
|
+
# Handle both MeasurementResult dict and raw float/nan values (for test compatibility)
|
|
223
|
+
if isinstance(thd_result, dict):
|
|
224
|
+
thd_val = thd_result["value"] if thd_result.get("applicable", True) else None
|
|
225
|
+
elif isinstance(thd_result, (int, float)): # type: ignore[unreachable]
|
|
226
|
+
thd_val = thd_result
|
|
227
|
+
else:
|
|
228
|
+
thd_val = None
|
|
229
|
+
|
|
201
230
|
result.update(
|
|
202
231
|
{
|
|
203
232
|
"peak_frequency": f"{peak_freq / 1e6:.3f} MHz",
|
|
204
|
-
"thd": f"{thd_val:.1f} dB"
|
|
233
|
+
"thd": f"{thd_val:.1f} dB"
|
|
234
|
+
if thd_val is not None and not np.isnan(thd_val)
|
|
235
|
+
else "N/A",
|
|
205
236
|
}
|
|
206
237
|
)
|
|
207
238
|
|
oscura/cli/characterize.py
CHANGED
|
@@ -243,12 +243,26 @@ def _add_buffer_results(results: dict[str, Any], trace: Any, logic_family: str)
|
|
|
243
243
|
os_pct = overshoot(trace)
|
|
244
244
|
us_pct = undershoot(trace)
|
|
245
245
|
|
|
246
|
+
# Extract values from MeasurementResult dicts
|
|
247
|
+
rt_val = rt["value"] if isinstance(rt, dict) and rt.get("applicable") else None
|
|
248
|
+
ft_val = ft["value"] if isinstance(ft, dict) and ft.get("applicable") else None
|
|
249
|
+
os_val = os_pct["value"] if isinstance(os_pct, dict) and os_pct.get("applicable") else None
|
|
250
|
+
us_val = us_pct["value"] if isinstance(us_pct, dict) and us_pct.get("applicable") else None
|
|
251
|
+
|
|
246
252
|
results.update(
|
|
247
253
|
{
|
|
248
|
-
"rise_time": f"{
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
"
|
|
254
|
+
"rise_time": f"{rt_val * 1e9:.2f} ns"
|
|
255
|
+
if rt_val is not None and not np.isnan(rt_val)
|
|
256
|
+
else "N/A",
|
|
257
|
+
"fall_time": f"{ft_val * 1e9:.2f} ns"
|
|
258
|
+
if ft_val is not None and not np.isnan(ft_val)
|
|
259
|
+
else "N/A",
|
|
260
|
+
"overshoot": f"{os_val:.1f} %"
|
|
261
|
+
if os_val is not None and not np.isnan(os_val)
|
|
262
|
+
else "N/A",
|
|
263
|
+
"undershoot": f"{us_val:.1f} %"
|
|
264
|
+
if us_val is not None and not np.isnan(us_val)
|
|
265
|
+
else "N/A",
|
|
252
266
|
"status": "PASS",
|
|
253
267
|
}
|
|
254
268
|
)
|
oscura/cli/export.py
CHANGED
|
@@ -195,14 +195,56 @@ def _export_csv(session: Any, output_path: Path) -> None:
|
|
|
195
195
|
|
|
196
196
|
|
|
197
197
|
def _export_matlab(session: Any, output_path: Path) -> None:
|
|
198
|
-
"""Export to MATLAB format.
|
|
198
|
+
"""Export to MATLAB .mat format.
|
|
199
|
+
|
|
200
|
+
Exports session data and traces to MATLAB-compatible .mat file using scipy.
|
|
199
201
|
|
|
200
202
|
Args:
|
|
201
|
-
session: Session object.
|
|
202
|
-
output_path: Output file path.
|
|
203
|
+
session: Session object with traces attribute.
|
|
204
|
+
output_path: Output file path (.mat extension).
|
|
205
|
+
|
|
206
|
+
Raises:
|
|
207
|
+
ImportError: If scipy is not installed.
|
|
203
208
|
"""
|
|
204
|
-
|
|
205
|
-
|
|
209
|
+
try:
|
|
210
|
+
from scipy.io import savemat
|
|
211
|
+
except ImportError as e:
|
|
212
|
+
raise ImportError(
|
|
213
|
+
"scipy is required for MATLAB export. Install with: pip install scipy"
|
|
214
|
+
) from e
|
|
215
|
+
|
|
216
|
+
import numpy as np
|
|
217
|
+
|
|
218
|
+
# Collect data to export
|
|
219
|
+
matlab_data: dict[str, Any] = {}
|
|
220
|
+
|
|
221
|
+
# Export traces if available
|
|
222
|
+
if hasattr(session, "traces") and session.traces:
|
|
223
|
+
for i, trace in enumerate(session.traces):
|
|
224
|
+
trace_name = f"trace_{i + 1}"
|
|
225
|
+
if hasattr(trace, "data"):
|
|
226
|
+
matlab_data[f"{trace_name}_data"] = np.asarray(trace.data)
|
|
227
|
+
if hasattr(trace, "metadata") and hasattr(trace.metadata, "sample_rate"):
|
|
228
|
+
matlab_data[f"{trace_name}_sample_rate"] = trace.metadata.sample_rate
|
|
229
|
+
|
|
230
|
+
# Export results if available
|
|
231
|
+
if hasattr(session, "results") and session.results:
|
|
232
|
+
if isinstance(session.results, dict):
|
|
233
|
+
for key, value in session.results.items():
|
|
234
|
+
# Convert to numpy-compatible format
|
|
235
|
+
if isinstance(value, (int, float, bool)):
|
|
236
|
+
matlab_data[key] = value
|
|
237
|
+
elif isinstance(value, (list, tuple)):
|
|
238
|
+
matlab_data[key] = np.array(value)
|
|
239
|
+
elif isinstance(value, np.ndarray):
|
|
240
|
+
matlab_data[key] = value
|
|
241
|
+
|
|
242
|
+
if not matlab_data:
|
|
243
|
+
matlab_data["message"] = "No data available for export"
|
|
244
|
+
|
|
245
|
+
# Save to .mat file
|
|
246
|
+
savemat(str(output_path), matlab_data)
|
|
247
|
+
click.echo(f"✓ Exported to MATLAB: {output_path}")
|
|
206
248
|
|
|
207
249
|
|
|
208
250
|
def _export_wireshark(session: Any, output_path: Path) -> None:
|
oscura/cli/main.py
CHANGED
|
@@ -248,6 +248,7 @@ from oscura.cli.compare import compare
|
|
|
248
248
|
from oscura.cli.config_cmd import config as config_cmd
|
|
249
249
|
from oscura.cli.decode import decode
|
|
250
250
|
from oscura.cli.export import export
|
|
251
|
+
from oscura.cli.pipeline import pipeline
|
|
251
252
|
from oscura.cli.validate_cmd import validate
|
|
252
253
|
from oscura.cli.visualize import visualize
|
|
253
254
|
|
|
@@ -312,6 +313,7 @@ cli.add_command(config_cmd, name="config")
|
|
|
312
313
|
cli.add_command(characterize) # type: ignore[has-type]
|
|
313
314
|
cli.add_command(batch) # type: ignore[has-type]
|
|
314
315
|
cli.add_command(compare) # type: ignore[has-type]
|
|
316
|
+
cli.add_command(pipeline) # type: ignore[has-type]
|
|
315
317
|
cli.add_command(shell)
|
|
316
318
|
cli.add_command(tutorial)
|
|
317
319
|
|
oscura/cli/onboarding/wizard.py
CHANGED
|
@@ -224,8 +224,8 @@ class AnalysisWizard:
|
|
|
224
224
|
else:
|
|
225
225
|
print(f" Sample rate: {rate / 1e3:.3f} kSa/s")
|
|
226
226
|
|
|
227
|
-
if hasattr(meta, "
|
|
228
|
-
print(f" Channel: {meta.
|
|
227
|
+
if hasattr(meta, "channel") and meta.channel:
|
|
228
|
+
print(f" Channel: {meta.channel}")
|
|
229
229
|
|
|
230
230
|
if hasattr(trace, "data"):
|
|
231
231
|
import numpy as np
|
|
@@ -375,13 +375,17 @@ class AnalysisWizard:
|
|
|
375
375
|
print(f" SNR: {snr_val:.1f} dB")
|
|
376
376
|
|
|
377
377
|
# Recommendations based on quality
|
|
378
|
-
|
|
378
|
+
# Extract values from MeasurementResult dicts
|
|
379
|
+
thd_value = thd_val["value"] if isinstance(thd_val, dict) else thd_val
|
|
380
|
+
snr_value = snr_val["value"] if isinstance(snr_val, dict) else snr_val
|
|
381
|
+
|
|
382
|
+
if thd_value is not None and thd_value > -40:
|
|
379
383
|
self.result.recommendations.append(
|
|
380
|
-
f"THD is {
|
|
384
|
+
f"THD is {thd_value:.1f} dB - consider filtering to reduce distortion"
|
|
381
385
|
)
|
|
382
|
-
if
|
|
386
|
+
if snr_value is not None and snr_value < 40:
|
|
383
387
|
self.result.recommendations.append(
|
|
384
|
-
f"SNR is {
|
|
388
|
+
f"SNR is {snr_value:.1f} dB - signal is noisy, try averaging or filtering"
|
|
385
389
|
)
|
|
386
390
|
|
|
387
391
|
if choice in (2, 3): # Anomalies
|