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
@@ -35,7 +35,7 @@ def interpolate(
35
35
  *,
36
36
  method: Literal["linear", "cubic", "nearest", "zero"] = "linear",
37
37
  fill_value: float | tuple[float, float] = np.nan,
38
- channel_name: str | None = None,
38
+ channel: str | None = None,
39
39
  ) -> WaveformTrace:
40
40
  """Interpolate trace to new time points.
41
41
 
@@ -47,7 +47,7 @@ def interpolate(
47
47
  new_time: New time points in seconds.
48
48
  method: Interpolation method ("linear", "cubic", "nearest", "zero").
49
49
  fill_value: Value for points outside original range.
50
- channel_name: Name for the result trace (optional).
50
+ channel: Name for the result trace (optional).
51
51
 
52
52
  Returns:
53
53
  Interpolated WaveformTrace at new time points.
@@ -70,13 +70,13 @@ def interpolate(
70
70
 
71
71
  # Create interpolator and interpolate
72
72
  interp_func = _create_interpolator(
73
- trace.time_vector, trace.data.astype(np.float64), method, fill_value
73
+ trace.time, trace.data.astype(np.float64), method, fill_value
74
74
  )
75
75
  result_data = interp_func(new_time)
76
76
 
77
77
  # Build result trace
78
78
  new_sample_rate = _calculate_new_sample_rate(new_time, trace.metadata.sample_rate)
79
- new_metadata = _create_interpolated_metadata(trace, new_sample_rate, channel_name)
79
+ new_metadata = _create_interpolated_metadata(trace, new_sample_rate, channel)
80
80
 
81
81
  return WaveformTrace(data=result_data.astype(np.float64), metadata=new_metadata)
82
82
 
@@ -107,7 +107,7 @@ def _calculate_new_sample_rate(new_time: NDArray[np.float64], original_sample_ra
107
107
 
108
108
 
109
109
  def _create_interpolated_metadata(
110
- trace: WaveformTrace, new_sample_rate: float, channel_name: str | None
110
+ trace: WaveformTrace, new_sample_rate: float, channel: str | None
111
111
  ) -> TraceMetadata:
112
112
  """Create metadata for interpolated trace."""
113
113
  return TraceMetadata(
@@ -117,7 +117,7 @@ def _create_interpolated_metadata(
117
117
  acquisition_time=trace.metadata.acquisition_time,
118
118
  trigger_info=trace.metadata.trigger_info,
119
119
  source_file=trace.metadata.source_file,
120
- channel_name=channel_name or f"{trace.metadata.channel_name or 'trace'}_interp",
120
+ channel=channel or f"{trace.metadata.channel or 'trace'}_interp",
121
121
  )
122
122
 
123
123
 
@@ -214,7 +214,7 @@ def resample(
214
214
  num_samples: int | None = None,
215
215
  method: Literal["fft", "polyphase", "interp"] = "fft",
216
216
  anti_alias: bool = True,
217
- channel_name: str | None = None,
217
+ channel: str | None = None,
218
218
  ) -> WaveformTrace:
219
219
  """Resample trace to new sample rate or number of samples.
220
220
 
@@ -232,7 +232,7 @@ def resample(
232
232
  - "polyphase": Polyphase filter resampling (efficient)
233
233
  - "interp": Linear interpolation (fastest)
234
234
  anti_alias: Apply anti-aliasing filter before downsampling.
235
- channel_name: Name for the result trace (optional).
235
+ channel: Name for the result trace (optional).
236
236
 
237
237
  Returns:
238
238
  Resampled WaveformTrace.
@@ -293,7 +293,7 @@ def resample(
293
293
  acquisition_time=trace.metadata.acquisition_time,
294
294
  trigger_info=trace.metadata.trigger_info,
295
295
  source_file=trace.metadata.source_file,
296
- channel_name=channel_name or f"{trace.metadata.channel_name or 'trace'}_resampled",
296
+ channel=channel or f"{trace.metadata.channel or 'trace'}_resampled",
297
297
  )
298
298
 
299
299
  return WaveformTrace(data=result_data.astype(np.float64), metadata=new_metadata)
@@ -305,7 +305,7 @@ def align_traces(
305
305
  *,
306
306
  method: Literal["interpolate", "resample"] = "interpolate",
307
307
  reference: Literal["first", "second", "higher"] = "higher",
308
- channel_names: tuple[str | None, str | None] | None = None,
308
+ channels: tuple[str | None, str | None] | None = None,
309
309
  ) -> tuple[WaveformTrace, WaveformTrace]:
310
310
  """Align two traces to have the same sample rate and length.
311
311
 
@@ -322,7 +322,7 @@ def align_traces(
322
322
  - "first": Use trace1's sample rate
323
323
  - "second": Use trace2's sample rate
324
324
  - "higher": Use the higher sample rate (default)
325
- channel_names: Optional names for the aligned traces.
325
+ channels: Optional names for the aligned traces.
326
326
 
327
327
  Returns:
328
328
  Tuple of (aligned_trace1, aligned_trace2) with matching parameters.
@@ -353,17 +353,17 @@ def align_traces(
353
353
  # Create common time vector
354
354
  common_time = np.arange(num_samples) / target_rate
355
355
 
356
- name1 = channel_names[0] if channel_names else None
357
- name2 = channel_names[1] if channel_names else None
356
+ name1 = channels[0] if channels else None
357
+ name2 = channels[1] if channels else None
358
358
 
359
359
  if method == "interpolate":
360
360
  # Interpolate both traces to common time points
361
- aligned1 = interpolate(trace1, common_time, channel_name=name1)
362
- aligned2 = interpolate(trace2, common_time, channel_name=name2)
361
+ aligned1 = interpolate(trace1, common_time, channel=name1)
362
+ aligned2 = interpolate(trace2, common_time, channel=name2)
363
363
  else: # "resample"
364
364
  # Resample both to common rate
365
- aligned1 = resample(trace1, num_samples=num_samples, channel_name=name1)
366
- aligned2 = resample(trace2, num_samples=num_samples, channel_name=name2)
365
+ aligned1 = resample(trace1, num_samples=num_samples, channel=name1)
366
+ aligned2 = resample(trace2, num_samples=num_samples, channel=name2)
367
367
 
368
368
  return aligned1, aligned2
369
369
 
@@ -374,7 +374,7 @@ def downsample(
374
374
  *,
375
375
  anti_alias: bool = True,
376
376
  method: Literal["decimate", "average", "max", "min"] = "decimate",
377
- channel_name: str | None = None,
377
+ channel: str | None = None,
378
378
  ) -> WaveformTrace:
379
379
  """Downsample trace by an integer factor.
380
380
 
@@ -390,7 +390,7 @@ def downsample(
390
390
  - "average": Average every N samples
391
391
  - "max": Maximum of every N samples
392
392
  - "min": Minimum of every N samples
393
- channel_name: Name for the result trace (optional).
393
+ channel: Name for the result trace (optional).
394
394
 
395
395
  Returns:
396
396
  Downsampled WaveformTrace.
@@ -440,7 +440,7 @@ def downsample(
440
440
  acquisition_time=trace.metadata.acquisition_time,
441
441
  trigger_info=trace.metadata.trigger_info,
442
442
  source_file=trace.metadata.source_file,
443
- channel_name=channel_name or f"{trace.metadata.channel_name or 'trace'}_ds{factor}",
443
+ channel=channel or f"{trace.metadata.channel or 'trace'}_ds{factor}",
444
444
  )
445
445
 
446
446
  return WaveformTrace(data=result_data, metadata=new_metadata)
@@ -1,15 +1,12 @@
1
- """Pipeline architecture for composable trace transformations.
1
+ """Reverse engineering pipeline for protocol analysis.
2
2
 
3
- This package provides pipeline composition, functional operators, and
4
- base classes for building reusable trace processing workflows.
3
+ This package provides the REPipeline for automated protocol reverse engineering
4
+ from captured traces, including flow extraction, payload analysis, field inference,
5
+ and protocol structure discovery.
5
6
 
6
7
  - RE-INT-001: RE Pipeline Integration
7
8
  """
8
9
 
9
- from .base import TraceTransformer
10
- from .composition import Composable, compose, curry, make_composable, pipe
11
- from .pipeline import Pipeline
12
-
13
10
  # RE-INT-001: Reverse Engineering Pipeline
14
11
  from .reverse_engineering import (
15
12
  FlowInfo,
@@ -22,22 +19,12 @@ from .reverse_engineering import (
22
19
  )
23
20
 
24
21
  __all__ = [
25
- "Composable",
26
22
  # RE-INT-001: Reverse Engineering Pipeline
27
23
  "FlowInfo",
28
24
  "MessageTypeInfo",
29
- # Pipeline
30
- "Pipeline",
31
25
  "ProtocolCandidate",
32
26
  "REAnalysisResult",
33
27
  "REPipeline",
34
28
  "StageResult",
35
- # Base classes
36
- "TraceTransformer",
37
29
  "analyze",
38
- # Composition
39
- "compose",
40
- "curry",
41
- "make_composable",
42
- "pipe",
43
30
  ]
@@ -242,10 +242,7 @@ def analyze_roi(
242
242
  sample_rate=trace.metadata.sample_rate,
243
243
  vertical_scale=trace.metadata.vertical_scale,
244
244
  vertical_offset=trace.metadata.vertical_offset,
245
- acquisition_time=trace.metadata.acquisition_time,
246
- trigger_info=trace.metadata.trigger_info,
247
- source_file=trace.metadata.source_file,
248
- channel_name=getattr(trace.metadata, "channel_name", None),
245
+ channel=trace.metadata.channel,
249
246
  ),
250
247
  )
251
248
 
@@ -78,7 +78,7 @@ class EdgeTrigger(Trigger):
78
78
  else:
79
79
  data = trace.data
80
80
 
81
- sample_period = trace.metadata.time_base
81
+ sample_period = 1.0 / trace.metadata.sample_rate
82
82
  events: list[TriggerEvent] = []
83
83
 
84
84
  if self.hysteresis > 0:
@@ -91,7 +91,7 @@ class PatternTrigger(Trigger):
91
91
  level = self._get_level(trace)
92
92
  digital = trace.data >= level
93
93
 
94
- sample_period = trace.metadata.time_base
94
+ sample_period = 1.0 / trace.metadata.sample_rate
95
95
  events: list[TriggerEvent] = []
96
96
 
97
97
  if self.match_type == "sequence":
@@ -240,7 +240,7 @@ class MultiChannelPatternTrigger(Trigger):
240
240
  digitals.append(trace.data >= level)
241
241
 
242
242
  # Find samples where all channels match pattern
243
- sample_period = traces[0].metadata.time_base
243
+ sample_period = 1.0 / traces[0].metadata.sample_rate
244
244
  n_samples = min(len(d) for d in digitals)
245
245
  events: list[TriggerEvent] = []
246
246
 
@@ -142,7 +142,7 @@ class PulseWidthTrigger(Trigger):
142
142
  data = trace.data
143
143
  level = self.level
144
144
 
145
- sample_period = trace.metadata.time_base
145
+ sample_period = 1.0 / trace.metadata.sample_rate
146
146
  pulses: list[PulseInfo] = []
147
147
 
148
148
  # Find all threshold crossings
@@ -419,7 +419,7 @@ class RuntTrigger(Trigger):
419
419
  return []
420
420
 
421
421
  data = trace.data
422
- sample_period = trace.metadata.time_base
422
+ sample_period = 1.0 / trace.metadata.sample_rate
423
423
  events: list[TriggerEvent] = []
424
424
 
425
425
  zones = np.array([self._get_zone(v) for v in data])
@@ -97,7 +97,7 @@ class WindowTrigger(Trigger):
97
97
  List of trigger events for window crossings.
98
98
  """
99
99
  data = trace.data
100
- sample_period = trace.metadata.time_base
100
+ sample_period = 1.0 / trace.metadata.sample_rate
101
101
  events: list[TriggerEvent] = []
102
102
 
103
103
  # Determine if each sample is inside the window
@@ -182,7 +182,7 @@ class ZoneTrigger(Trigger):
182
182
  List of trigger events.
183
183
  """
184
184
  data = trace.data
185
- sample_period = trace.metadata.time_base
185
+ sample_period = 1.0 / trace.metadata.sample_rate
186
186
  time_vector = np.arange(len(data)) * sample_period
187
187
  events: list[TriggerEvent] = []
188
188
 
@@ -396,7 +396,7 @@ class MaskTrigger(Trigger):
396
396
  mask_path = Path(self.mask_points)
397
397
 
398
398
  data = trace.data
399
- sample_period = trace.metadata.time_base
399
+ sample_period = 1.0 / trace.metadata.sample_rate
400
400
  time_vector = np.arange(len(data)) * sample_period
401
401
 
402
402
  # Create points array for containment test
@@ -35,7 +35,7 @@ from enum import Enum
35
35
  from typing import Any, Protocol
36
36
 
37
37
  try:
38
- import can # type: ignore[import-untyped]
38
+ import can
39
39
  except ImportError:
40
40
  can = None # type: ignore[assignment]
41
41
 
@@ -46,18 +46,18 @@ except ImportError:
46
46
  # Create module structure for test patching even when pyusb unavailable
47
47
  import types
48
48
 
49
- usb = types.ModuleType("usb") # type: ignore[assignment]
49
+ usb = types.ModuleType("usb")
50
50
  usb.core = None # type: ignore[attr-defined]
51
51
 
52
52
  try:
53
53
  import spidev # type: ignore[import-not-found]
54
54
  except ImportError:
55
- spidev = None # type: ignore[assignment]
55
+ spidev = None
56
56
 
57
57
  try:
58
58
  from smbus2 import SMBus # type: ignore[import-not-found]
59
59
  except ImportError:
60
- SMBus = None # type: ignore[assignment]
60
+ SMBus = None
61
61
 
62
62
  try:
63
63
  import RPi.GPIO as GPIO # type: ignore[import-untyped]
@@ -65,23 +65,23 @@ except ImportError:
65
65
  try:
66
66
  import gpiod # type: ignore[import-not-found]
67
67
 
68
- GPIO = None # type: ignore[assignment]
68
+ GPIO = None
69
69
  except ImportError:
70
- GPIO = None # type: ignore[assignment]
71
- gpiod = None # type: ignore[assignment]
70
+ GPIO = None
71
+ gpiod = None
72
72
 
73
73
  try:
74
74
  from scapy.all import IP, UDP, Packet, wrpcap # type: ignore[attr-defined]
75
75
  except ImportError:
76
- IP = None # type: ignore[assignment]
77
- UDP = None # type: ignore[assignment]
76
+ IP = None
77
+ UDP = None
78
78
  Packet = None # type: ignore[assignment,misc]
79
79
  wrpcap = None # type: ignore[assignment]
80
80
 
81
81
  try:
82
82
  import serial # type: ignore[import-untyped]
83
83
  except ImportError:
84
- serial = None # type: ignore[assignment]
84
+ serial = None
85
85
 
86
86
  from oscura.utils.serial import connect_serial_port
87
87
 
@@ -960,7 +960,7 @@ class HILTester:
960
960
  # Create UDP packet for received data if present
961
961
  if recv_data:
962
962
  pkt = IP(src="192.168.1.1") / UDP(sport=12345) / bytes(recv_data)
963
- pkt.time = timestamp + 0.001 # Slight offset
963
+ pkt.time = timestamp + 0.001
964
964
  packets.append(pkt)
965
965
 
966
966
  if packets: