PyQa40x-i2c 0.1.2__tar.gz → 0.2.0__tar.gz

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 (65) hide show
  1. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.0}/PKG-INFO +1 -1
  2. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.0}/RELEASE_NOTES.md +4 -0
  3. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.0}/src/PyQa40x/_version.py +3 -3
  4. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.0}/src/PyQa40x/analyzer.py +123 -147
  5. pyqa40x_i2c-0.2.0/src/PyQa40x/stream.py +253 -0
  6. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.0}/src/PyQa40x_i2c.egg-info/PKG-INFO +1 -1
  7. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.0}/src/PyQa40x_i2c.egg-info/SOURCES.txt +2 -0
  8. pyqa40x_i2c-0.2.0/src/PyQa40x_i2c.egg-info/scm_file_list.json +58 -0
  9. pyqa40x_i2c-0.2.0/src/PyQa40x_i2c.egg-info/scm_version.json +8 -0
  10. pyqa40x_i2c-0.1.2/src/PyQa40x/stream.py +0 -208
  11. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.0}/.github/workflows/code_checker.yml +0 -0
  12. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.0}/.github/workflows/publish_package.yml +0 -0
  13. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.0}/.github/workflows/unit_tests.yml +0 -0
  14. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.0}/.gitignore +0 -0
  15. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.0}/.pylint_ignore +0 -0
  16. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.0}/LICENSE +0 -0
  17. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.0}/LibTest/FirstPrinciples.py +0 -0
  18. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.0}/LibTest/PyQa40x_acoustic_chirp_spl_bt.py +0 -0
  19. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.0}/LibTest/PyQa40x_acoustic_dbspl.py +0 -0
  20. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.0}/LibTest/PyQa40x_acoustic_energy_decay_bt.py +0 -0
  21. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.0}/LibTest/PyQa40x_coherent_averaging.py +0 -0
  22. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.0}/LibTest/PyQa40x_cross_correlation.py +0 -0
  23. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.0}/LibTest/PyQa40x_freq_response_with_mask.py +0 -0
  24. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.0}/LibTest/PyQa40x_pinknoise.py +0 -0
  25. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.0}/LibTest/PyQa40x_thdn.py +0 -0
  26. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.0}/LibTest/mask_freq.txt +0 -0
  27. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.0}/LibTest/mask_phase.txt +0 -0
  28. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.0}/MANIFEST.in +0 -0
  29. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.0}/README.md +0 -0
  30. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.0}/_ci/is_dev_version.sh +0 -0
  31. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.0}/pyproject.toml +0 -0
  32. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.0}/requirements-dev.txt +0 -0
  33. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.0}/requirements-test.txt +0 -0
  34. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.0}/requirements.txt +0 -0
  35. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.0}/setup.cfg +0 -0
  36. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.0}/src/PyQa40x/__init__.py +0 -0
  37. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.0}/src/PyQa40x/analyzer_params.py +0 -0
  38. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.0}/src/PyQa40x/bluetooth.py +0 -0
  39. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.0}/src/PyQa40x/control.py +0 -0
  40. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.0}/src/PyQa40x/cross_correlation.py +0 -0
  41. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.0}/src/PyQa40x/fft_processor.py +0 -0
  42. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.0}/src/PyQa40x/freq_series_plotter.py +0 -0
  43. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.0}/src/PyQa40x/helpers.py +0 -0
  44. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.0}/src/PyQa40x/math_chirp.py +0 -0
  45. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.0}/src/PyQa40x/math_energy.py +0 -0
  46. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.0}/src/PyQa40x/math_general.py +0 -0
  47. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.0}/src/PyQa40x/math_sines.py +0 -0
  48. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.0}/src/PyQa40x/math_windows.py +0 -0
  49. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.0}/src/PyQa40x/registers.py +0 -0
  50. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.0}/src/PyQa40x/series_plotter.py +0 -0
  51. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.0}/src/PyQa40x/sig_proc.py +0 -0
  52. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.0}/src/PyQa40x/wave.py +0 -0
  53. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.0}/src/PyQa40x/wave_chirp.py +0 -0
  54. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.0}/src/PyQa40x/wave_sine.py +0 -0
  55. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.0}/src/PyQa40x.egg-info/PKG-INFO +0 -0
  56. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.0}/src/PyQa40x.egg-info/SOURCES.txt +0 -0
  57. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.0}/src/PyQa40x.egg-info/dependency_links.txt +0 -0
  58. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.0}/src/PyQa40x.egg-info/requires.txt +0 -0
  59. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.0}/src/PyQa40x.egg-info/top_level.txt +0 -0
  60. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.0}/src/PyQa40x_i2c.egg-info/dependency_links.txt +0 -0
  61. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.0}/src/PyQa40x_i2c.egg-info/requires.txt +0 -0
  62. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.0}/src/PyQa40x_i2c.egg-info/top_level.txt +0 -0
  63. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.0}/tests/__init__.py +0 -0
  64. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.0}/tests/test_template.py +0 -0
  65. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.0}/tox.ini +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyQa40x-i2c
3
- Version: 0.1.2
3
+ Version: 0.2.0
4
4
  Summary: A Python library for the QA40x audio analyser (int2code fork)
5
5
  Maintainer-email: Hubert Stepniewski <hubert.stepniewski@int2code.com>
6
6
  License: MIT License
@@ -1,3 +1,7 @@
1
+ ### 0.2.0
2
+
3
+ [MINOR] Add endless audio capturing
4
+
1
5
  ### 0.1.2
2
6
 
3
7
  [PATCH] Fix usb teardown deadlock.
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '0.1.2'
22
- __version_tuple__ = version_tuple = (0, 1, 2)
21
+ __version__ = version = '0.2.0'
22
+ __version_tuple__ = version_tuple = (0, 2, 0)
23
23
 
24
- __commit_id__ = commit_id = 'g1bd064f6c'
24
+ __commit_id__ = commit_id = 'g6a5ba3357'
@@ -1,21 +1,24 @@
1
+ import threading
2
+ import logging
3
+
1
4
  import numpy as np
2
5
  import usb1 # pip install libusb1
3
- import struct
4
6
  import atexit
5
- import scipy.signal # pip install scipy
6
7
 
7
8
  from PyQa40x.registers import Registers
8
9
  from PyQa40x.control import Control
9
10
  from PyQa40x.stream import Stream
10
- from PyQa40x.wave_sine import Wave
11
- from PyQa40x.fft_processor import FFTProcessor
12
- from PyQa40x.sig_proc import SigProc
13
11
  from PyQa40x.helpers import *
14
12
  from PyQa40x.analyzer_params import AnalyzerParams
15
13
  from PyQa40x.bluetooth import BluetoothAudioDevice
16
14
 
15
+ logger = logging.getLogger(__name__)
16
+
17
17
 
18
18
  class Analyzer:
19
+
20
+ CAPTURE_CHUNK_BYTES = Stream.TRANSFER_BYTES # bytes per USB transfer
21
+
19
22
  def __init__(self):
20
23
  """
21
24
  Initializes the Analyzer class.
@@ -29,6 +32,9 @@ class Analyzer:
29
32
  self.cal_data: dict | None = None
30
33
  self.bt_device: BluetoothAudioDevice | None = None
31
34
  self._cleanup_registered = False
35
+ self._capturing: bool = False
36
+ self._dac_writer: threading.Thread | None = None # set in start_capture()
37
+ self._dac_writer_error: Exception | None = None # error from DAC writer thread
32
38
 
33
39
  def init(
34
40
  self,
@@ -139,126 +145,137 @@ class Analyzer:
139
145
  except Exception as e:
140
146
  print(f"An error occurred during cleanup: {e}")
141
147
 
142
- def send_receive(self, left_wave: Wave, right_wave: Wave) -> tuple[Wave, Wave]:
148
+ @staticmethod
149
+ def list_audio_devices_by_sample_rate(target_sample_rate: int):
150
+ """
151
+ List all available audio devices that support a specific sample rate
152
+ by calling the method from BluetoothAudioDevice.
143
153
  """
144
- Sends DAC data to the device and receives ADC data.
154
+ BluetoothAudioDevice.list_audio_devices_by_sample_rate(target_sample_rate)
155
+
156
+ def _convert_adc_bytes_to_volts(
157
+ self, raw_bytes: bytearray, n_samples: int
158
+ ) -> tuple[np.ndarray, np.ndarray]:
159
+ """
160
+ Convert interleaved int32 ADC bytes to calibrated Volt arrays.
145
161
 
146
162
  Args:
147
- left_wave (Wave): Wave instance with left channel DAC data.
148
- right_wave (Wave): Wave instance with right channel DAC data.
163
+ raw_bytes (bytearray): Interleaved little-endian int32 stereo ADC data.
164
+ n_samples (int): Samples per channel to return.
149
165
 
150
166
  Returns:
151
- tuple[Wave, Wave]: Tuple containing Wave instances with left and right channel ADC data.
167
+ tuple[np.ndarray, np.ndarray]: (left_volts, right_volts), float64.
152
168
  """
153
- left_dac_data = left_wave.get_buffer()
154
- right_dac_data = right_wave.get_buffer()
169
+ # Reads the raw bytes and treat every 4 bytes as one signed 32‑bit integer
170
+ # [L0, R0, L1, R1, L2, R2, …]
171
+ interleaved = np.frombuffer(raw_bytes, dtype=np.int32)
155
172
 
156
- # On QA402 and QA403, outputs need to be swapped
157
- left_dac_data, right_dac_data = right_dac_data, left_dac_data
173
+ # Split interleaved int32 array into left and right channels
174
+ left_int = interleaved[0::2][:n_samples]
175
+ right_int = interleaved[1::2][:n_samples]
158
176
 
159
- left_peak = np.max(left_dac_data)
177
+ # Convert int32 → float64 and normalise to range +-1
178
+ max_int_value = 2**31 - 1
179
+ left_f = left_int.astype(np.float64) / max_int_value
180
+ right_f = right_int.astype(np.float64) / max_int_value
160
181
 
161
- # Get calibration factors for outgoing data
162
- cal_dac_left, cal_dac_right = self.control.get_dac_cal(
163
- self.cal_data, self.params.max_output_level
182
+ # Factor for computing the voltage that corresponds to digital full scale
183
+ dbfs_correction = 10 ** ((self.params.max_input_level - 6) / 20)
184
+ # Stored QA40x calibration factors for the current input level.
185
+ cal_adc_left, cal_adc_right = self.control.get_adc_cal(
186
+ self.cal_data, self.params.max_input_level
164
187
  )
165
188
 
166
- # Convert to dBFS. Note the 3 dB adjustment--dBFS is peak, while dBV is RMS
167
- dac_dbfs_adjustment = 10 ** -((self.params.max_output_level + 3) / 20)
168
-
169
- # Apply calibration and dBFS scaling
170
- left_dac_data = left_dac_data * dac_dbfs_adjustment * cal_dac_left
171
- right_dac_data = right_dac_data * dac_dbfs_adjustment * cal_dac_right
172
-
173
- # Convert incoming doubles to float
174
- left_dac_data_float = left_dac_data.astype(np.float32)
175
- right_dac_data_float = right_dac_data.astype(np.float32)
176
-
177
- # Interleave the left and right channels
178
- interleaved_dac_data = np.empty(
179
- (left_dac_data_float.size + right_dac_data_float.size,), dtype=np.float32
180
- )
181
- interleaved_dac_data[0::2] = left_dac_data_float
182
- interleaved_dac_data[1::2] = right_dac_data_float
189
+ # Apply ADC calibration and dBFS → Volts conversion
190
+ left_volts = left_f * cal_adc_left * dbfs_correction
191
+ right_volts = right_f * cal_adc_right * dbfs_correction
183
192
 
184
- # Convert to bytes, multiplying by max int value
185
- max_int_value = 2**31 - 1
186
- interleaved_dac_data = (interleaved_dac_data * max_int_value).astype(np.int32)
193
+ return left_volts, right_volts
187
194
 
188
- # Pack the data into chunks of 16k bytes
189
- chunk_size = 16384 # 16k bytes
190
- num_ints_per_chunk = chunk_size // 4 # 32-bit ints, so 4 bytes per int
191
- total_chunks = len(interleaved_dac_data) // num_ints_per_chunk
195
+ def start_capture(self) -> None:
196
+ """
197
+ Starts a continuous capture of ADC data in a background thread.
198
+ The captured data can be retrieved later using the `read_capture` method.
199
+ """
200
+ if self._capturing:
201
+ raise RuntimeError("Capture is already in progress.")
192
202
 
203
+ self._capturing = True
204
+ self._dac_writer_error = None
193
205
  self.stream.start()
206
+ self._dac_writer = threading.Thread(
207
+ target=self._dac_writer_loop, name="qa40x-dac-writer-thread", daemon=True
208
+ )
209
+ self._dac_writer.start()
210
+
211
+ def _dac_writer_loop(self) -> None:
212
+ """Continuously submit zero-DAC / ADC-read transfer pairs while capturing."""
213
+ while self._capturing and self.stream.running:
214
+ try:
215
+ self.stream.write_zeros(self.CAPTURE_CHUNK_BYTES)
216
+ except Exception as exc: # pylint: disable=broad-except
217
+ # Racing with stop() is expected; a real failure while we still
218
+ # expect to run is recorded in the post-loop reconciliation below.
219
+ if self._capturing and self.stream.running:
220
+ self._dac_writer_error = exc
221
+ logger.error("DAC writer failed; stopping capture.", exc_info=exc)
222
+ break
223
+
224
+ # If the loop ended but we never asked to stop, the stream halted
225
+ # underneath us (error or ADC overflow): record it and clear _capturing so
226
+ # the analyzer isn't wedged "capturing" behind a dead writer thread.
227
+ if self._capturing:
228
+ self._capturing = False
229
+ if self._dac_writer_error is None:
230
+ self._dac_writer_error = self.stream.overflow_error or RuntimeError(
231
+ "Stream stopped unexpectedly during capture."
232
+ )
233
+
234
+ def read_capture(self, n_samples: int) -> tuple[np.ndarray, np.ndarray]:
235
+ """
236
+ Return the next `n_samples` samples per channel as Volt arrays.
194
237
 
195
- # Play wave on Bluetooth if available
196
- bt_thread = None
197
- if self.bt_device:
198
- # Play both left and right waves on Bluetooth
199
- bt_thread = self.bt_device.play_wave_background(left_wave, right_wave)
200
-
201
- try:
202
- for i in range(total_chunks):
203
- chunk = interleaved_dac_data[
204
- i * num_ints_per_chunk : (i + 1) * num_ints_per_chunk
205
- ]
206
- buffer = struct.pack("<%di" % len(chunk), *chunk)
207
- self.stream.write(buffer)
208
- finally:
209
- self.stream.stop()
210
-
211
- # Collect ADC data. This is bytes
212
- interleaved_adc_data = self.stream.collect_remaining_adc_data()
213
-
214
- # Wait for the Bluetooth thread to finish after processing
215
- if bt_thread:
216
- bt_thread.join()
217
-
218
- # Convert collected ADC data back to int
219
- interleaved_adc_data = np.frombuffer(interleaved_adc_data, dtype=np.int32)
220
-
221
- # Separate interleaved ADC data into left and right channels. This is int
222
- left_adc_data_int = interleaved_adc_data[0::2]
223
- right_adc_data_int = interleaved_adc_data[1::2]
238
+ Blocks until the samples are available. Consecutive calls return
239
+ contiguous audio (no gap) because the stream is never stopped between
240
+ reads. Returns fewer than `n_samples` only if the capture is stopping.
224
241
 
225
- # Convert left and right channels back to double
226
- left_adc_data = left_adc_data_int.astype(np.float64) / max_int_value
227
- right_adc_data = right_adc_data_int.astype(np.float64) / max_int_value
242
+ Args:
243
+ n_samples (int): Samples per channel to return.
228
244
 
229
- # Get calibration factors for incoming data
230
- cal_adc_left, cal_adc_right = self.control.get_adc_cal(
231
- self.cal_data, self.params.max_input_level
232
- )
245
+ Returns:
246
+ tuple[np.ndarray, np.ndarray]: (left_volts, right_volts), float64.
233
247
 
234
- # Convert from dBFS to dBV. Note the 6 dB factor--the ADC is differential
235
- adc_dbfs_correction = 10 ** ((self.params.max_input_level - 6) / 20)
248
+ Raises:
249
+ RuntimeError: If the DAC writer thread failed during capture.
250
+ AdcOverflowError: If capture data was not read fast enough.
251
+ """
252
+ if self._dac_writer_error is not None:
253
+ raise RuntimeError(
254
+ "DAC writer thread encountered an error during capture."
255
+ ) from self._dac_writer_error
236
256
 
237
- # Apply calibration and dBFS scaling
238
- left_adc_data = left_adc_data * cal_adc_left * adc_dbfs_correction
239
- right_adc_data = right_adc_data * cal_adc_right * adc_dbfs_correction
257
+ need_bytes = n_samples * 8 # 2 channels x int32
240
258
 
241
- # Ensure the buffer matches the expected shape. Is this needed?
242
- expected_shape = (
243
- self.params.pre_buf + self.params.fft_size + self.params.post_buf,
259
+ raw = self.stream.consume_adc(
260
+ need_bytes, should_keep_waiting=lambda: self._capturing
244
261
  )
245
- if left_adc_data.shape[0] != expected_shape[0]:
246
- left_adc_data = np.resize(left_adc_data, expected_shape)
247
- if right_adc_data.shape[0] != expected_shape[0]:
248
- right_adc_data = np.resize(right_adc_data, expected_shape)
249
-
250
- # Create instances of Wave with the full buffers
251
- left_wave = Wave(self.params)
252
- right_wave = Wave(self.params)
253
- left_wave.set_buffer(left_adc_data)
254
- right_wave.set_buffer(right_adc_data)
262
+ got_samples = len(raw) // 8
263
+ return self._convert_adc_bytes_to_volts(raw, got_samples)
255
264
 
256
- return left_wave, right_wave
265
+ def stop_capture(self) -> None:
266
+ """
267
+ Stop the continuous capture: end the feeder thread and stop the stream.
257
268
 
258
- @staticmethod
259
- def list_audio_devices_by_sample_rate(target_sample_rate: int):
260
- """List all available audio devices that support a specific sample rate by calling the method from BluetoothAudioDevice."""
261
- BluetoothAudioDevice.list_audio_devices_by_sample_rate(target_sample_rate)
269
+ Order matters: clearing `_capturing` and stopping the stream releases a
270
+ dac writer blocked on the dacQueue (stop() cancels transfers and drains the
271
+ queue), after which the dac writer observes `_capturing == False` and exits.
272
+ """
273
+ if not self._capturing:
274
+ return
275
+ self._capturing = False
276
+ self.stream.stop()
277
+ if self._dac_writer is not None:
278
+ self._dac_writer.join(timeout=5.0)
262
279
 
263
280
  def capture(self, total_samples: int) -> tuple[np.ndarray, np.ndarray]:
264
281
  """
@@ -285,51 +302,10 @@ class Analyzer:
285
302
  two float64 arrays each of length ``total_samples``, values
286
303
  in Volts (calibrated, same scale as ``send_receive()``).
287
304
  """
288
- chunk_bytes = 16384 # 16 kB per USB transfer
289
- bytes_per_stereo_sample = 8 # 2 × int32
290
- need_bytes = total_samples * bytes_per_stereo_sample
291
-
292
- # Number of DAC write / ADC read pairs needed to cover need_bytes.
293
- # Each pair transfers chunk_bytes ADC bytes.
294
- n_chunks = -(-need_bytes // chunk_bytes) # ceiling division
295
-
296
- max_int_value = 2**31 - 1
297
-
298
- # -- single stream start ------------------------------------------------
299
- self.stream.start()
305
+ # A fixed-length capture is just a continuous capture read once: start
306
+ # the gapless stream, read exactly total_samples, then stop.
307
+ self.start_capture()
300
308
  try:
301
- # Submit all zero-DAC / ADC-read pairs up front so the USB host
302
- # controller can pipeline them; no stop/restart in the middle.
303
- for _ in range(n_chunks):
304
- self.stream.write_zeros(chunk_bytes)
305
-
306
- # Block until we have exactly the bytes we need
307
- raw_bytes = self.stream.collect_adc_exact(need_bytes)
309
+ return self.read_capture(total_samples)
308
310
  finally:
309
- self.stream.stop()
310
- # -- stream stopped -----------------------------------------------------
311
-
312
- # Deserialise interleaved int32 ADC data
313
- interleaved = np.frombuffer(raw_bytes, dtype=np.int32)
314
-
315
- left_int = interleaved[0::2]
316
- right_int = interleaved[1::2]
317
-
318
- # Trim to exact length (collect_adc_exact already trims, but be safe)
319
- left_int = left_int[:total_samples]
320
- right_int = right_int[:total_samples]
321
-
322
- # Convert int32 → float64 in the range ±1
323
- left_f = left_int.astype(np.float64) / max_int_value
324
- right_f = right_int.astype(np.float64) / max_int_value
325
-
326
- # Apply ADC calibration and dBFS → Volts conversion
327
- cal_adc_left, cal_adc_right = self.control.get_adc_cal(
328
- self.cal_data, self.params.max_input_level
329
- )
330
- adc_dbfs_correction = 10 ** ((self.params.max_input_level - 6) / 20)
331
-
332
- left_volts = left_f * cal_adc_left * adc_dbfs_correction
333
- right_volts = right_f * cal_adc_right * adc_dbfs_correction
334
-
335
- return left_volts, right_volts
311
+ self.stop_capture()
@@ -0,0 +1,253 @@
1
+ import threading
2
+ import queue
3
+ import usb1
4
+ import logging
5
+ from typing import Callable
6
+
7
+ from PyQa40x.registers import Registers
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ class AdcOverflowError(RuntimeError):
13
+ """
14
+ Raised when the ADC receive buffer exceeds its high-water mark.
15
+
16
+ The capture pipeline requires the consumer to read captured data
17
+ continuously (see ``consume_adc``). If the consumer falls behind, the
18
+ receive buffer grows without bound toward OOM. Rather than leak memory,
19
+ the stream treats this as a hard error: the worker thread latches it and
20
+ ``consume_adc`` re-raises it on the consumer's thread.
21
+ """
22
+
23
+
24
+ class Stream:
25
+ """
26
+ Class to manage buffer streaming, with event worker thread.
27
+ """
28
+
29
+ TRANSFER_TIMEOUT_MS = 1000 # Timeout for USB transfers in milliseconds
30
+ TRANSFER_BYTES = 16384 # 16 kB per USB bulk transfer (hardware chunk size)
31
+ # cap on unread ADC data before overflow
32
+ MAX_BUFFERED_BYTES = 256 * 1024 * 1024
33
+
34
+ def __init__(
35
+ self,
36
+ context: usb1.USBContext,
37
+ device: usb1.USBDeviceHandle,
38
+ registers: Registers,
39
+ ) -> None:
40
+ """
41
+ Initialize the Stream.
42
+
43
+ Args:
44
+ context (usb1.USBContext): The USB context.
45
+ device (usb1.USBDeviceHandle): The USB device handle.
46
+ registers (Registers): The Registers instance for control writes.
47
+ """
48
+ self.context = context
49
+ self.device = device
50
+ self.registers = registers
51
+ self.endpoint_read = usb1.ENDPOINT_IN | 0x02 # EP2 in
52
+ self.endpoint_write = usb1.ENDPOINT_OUT | 0x02 # EP2 out
53
+ self.dacQueue = queue.Queue(
54
+ maxsize=5
55
+ ) # Max. 5 overlapping buffers in flight, block on more
56
+ self.adcQueue = queue.Queue() # Unlimited queue for received data buffers
57
+ self.transfer_helper = usb1.USBTransferHelper() # Use the callback dispatcher
58
+ # Handle COMPLETED transfers normally; also handle ERROR/TIMED_OUT/CANCELLED
59
+ # so the queues are always drained and dacQueue.put() never deadlocks.
60
+ for status in (
61
+ usb1.TRANSFER_COMPLETED,
62
+ usb1.TRANSFER_ERROR,
63
+ usb1.TRANSFER_TIMED_OUT,
64
+ usb1.TRANSFER_CANCELLED,
65
+ usb1.TRANSFER_STALL,
66
+ usb1.TRANSFER_NO_DEVICE,
67
+ usb1.TRANSFER_OVERFLOW,
68
+ ):
69
+ self.transfer_helper.setEventCallback(status, self.callback)
70
+ self.thread: threading.Thread | None = None
71
+ self.running = False
72
+ # In-flight transfers, keyed by identity; pruned in callback, bulk-cancelled in stop.
73
+ self._pending_transfers: set = set()
74
+ # raised to break the worker out of stuck queues
75
+ self._force_stop = threading.Event()
76
+ self._transfer_lock = threading.Lock() # protects _pending_transfers
77
+ self._epoch = 0 # session counter; stale-session callbacks are ignored
78
+ self._adc_data = queue.Queue() # received ADC data, one chunk per item
79
+ self._adc_data_carry = bytearray() # leftover ADC bytes between consume_adc calls
80
+ # latched, raised by consume_adc
81
+ self._overflow_error: AdcOverflowError | None = None
82
+ self._max_queued_chunks = self.MAX_BUFFERED_BYTES // self.TRANSFER_BYTES
83
+
84
+ @property
85
+ def overflow_error(self) -> "AdcOverflowError | None":
86
+ """Latched ADC overflow error, or None if the buffer never overran."""
87
+ return self._overflow_error
88
+
89
+ def start(self) -> None:
90
+ """
91
+ Start streaming and spawn the worker thread.
92
+ """
93
+ self._force_stop.clear()
94
+ self._epoch += 1 # new session; callbacks from prior sessions are ignored
95
+ self.thread = threading.Thread(target=self.worker, daemon=True)
96
+ self.running = True
97
+ self.thread.start()
98
+ self._adc_data = queue.Queue() # Reset the ADC data queue
99
+ self._adc_data_carry = bytearray() # Reset the carry buffer
100
+ self._overflow_error = None # Reset the overflow error
101
+ self.registers.write(8, 0x05) # Start streaming
102
+
103
+ def stop(self) -> None:
104
+ """
105
+ Stop streaming and end the worker thread.
106
+
107
+ Sets ``running`` to False and cancels in-flight USB transfers so their
108
+ callbacks fire and drain the queues, which allows the worker loop to
109
+ exit naturally. If the worker has not exited within 5 seconds (e.g.
110
+ because a USB cancellation is slow), ``_force_stop`` is set so the
111
+ worker breaks unconditionally after its current
112
+ ``handleEventsTimeout(0.1)`` call (≤ 100 ms), and we give it one
113
+ further second to join before proceeding.
114
+ """
115
+ # Cancel any in-flight transfers so their callbacks fire and drain the queues,
116
+ # allowing the worker thread to exit its loop. The lock ensure no transfer is added
117
+ # to the list while we are cancelling.
118
+ with self._transfer_lock:
119
+ self.running = False
120
+ for transfer in self._pending_transfers:
121
+ try:
122
+ transfer.cancel()
123
+ except Exception: # pylint: disable=broad-except
124
+ pass
125
+ self._pending_transfers.clear()
126
+
127
+ if self.thread is not None and self.thread.is_alive():
128
+ self.thread.join(timeout=5.0)
129
+ if self.thread.is_alive():
130
+ # Worker is stuck (slow USB cancellation). Force it out so
131
+ # context.close() does not deadlock.
132
+ self._force_stop.set()
133
+ self.thread.join(timeout=1.0)
134
+ self.registers.write(8, 0x00) # Stop streaming
135
+
136
+ def worker(self) -> None:
137
+ """
138
+ Event loop for the asynchronous transfers.
139
+
140
+ Runs until *both* conditions are true:
141
+ * ``running`` is False (``stop()`` has been called), and
142
+ * both USB queues are empty (all callbacks have fired).
143
+
144
+ An early exit is also triggered by ``_force_stop`` (set by ``stop()``
145
+ when the normal join times out) so that ``context.close()`` is never
146
+ called while this thread is still inside ``handleEventsTimeout``.
147
+ """
148
+ while self.running or not (
149
+ self.dacQueue.empty() and self.adcQueue.empty()
150
+ ): # Play until the last
151
+ if self._force_stop.is_set():
152
+ break
153
+ # 100 ms timeout prevents blocking forever
154
+ self.context.handleEventsTimeout(0.1)
155
+
156
+ def callback(self, transfer: usb1.USBTransfer) -> None:
157
+ """
158
+ Handle a completed transfer (runs on the worker thread).
159
+
160
+ On a completed ADC read, buffer the bytes; if the receive queue passes
161
+ its limit, latch an AdcOverflowError and stop so consume_adc can raise it.
162
+
163
+ Args:
164
+ transfer (usb1.USBTransfer): The USB transfer that has completed.
165
+ """
166
+ current = transfer.getUserData() == self._epoch # ignore stale-session data
167
+ if transfer.getEndpoint() == self.endpoint_read:
168
+ if current and transfer.getStatus() == usb1.TRANSFER_COMPLETED:
169
+ if self._overflow_error is None:
170
+ # Copy: the transfer buffer is freed once we prune the transfer.
171
+ self._adc_data.put(bytes(transfer.getBuffer()))
172
+ if self._adc_data.qsize() > self._max_queued_chunks:
173
+ self._overflow_error = AdcOverflowError(
174
+ "ADC receive queue exceeded high-water mark "
175
+ f"({self._adc_data.qsize()} chunks); consumer not "
176
+ "reading capture fast enough."
177
+ )
178
+ self.running = False # unwind feeder + worker
179
+ logger.error(str(self._overflow_error))
180
+ self.adcQueue.get_nowait() # Always drain the queue
181
+ else:
182
+ self.dacQueue.get_nowait() # Always drain the queue
183
+
184
+ # Prune this finished transfer so _pending_transfers stays bounded during
185
+ # a long capture; discard() is a no-op if stop() already cleared it.
186
+ with self._transfer_lock:
187
+ self._pending_transfers.discard(transfer)
188
+
189
+ def write(self, buffer: bytes) -> None:
190
+ """Submit one DAC playback buffer and one matching ADC read transfer."""
191
+ with self._transfer_lock:
192
+ if not self.running:
193
+ raise RuntimeError(
194
+ "Stream is not running; call start() first.")
195
+ dac = self.device.getTransfer()
196
+ dac.setBulk(self.endpoint_write, buffer, self.transfer_helper,
197
+ self._epoch, self.TRANSFER_TIMEOUT_MS)
198
+ dac.submit()
199
+ self._pending_transfers.add(dac)
200
+ read = self.device.getTransfer()
201
+ read.setBulk(self.endpoint_read, len(buffer), self.transfer_helper,
202
+ self._epoch, self.TRANSFER_TIMEOUT_MS)
203
+ read.submit()
204
+ self._pending_transfers.add(read)
205
+ self.dacQueue.put(dac)
206
+ self.adcQueue.put(read)
207
+
208
+ def write_zeros(self, chunk_bytes: int = TRANSFER_BYTES) -> None:
209
+ """Feed the ADC pipeline with a zero DAC buffer (continuous capture)."""
210
+ self.write(bytes(chunk_bytes))
211
+
212
+ def consume_adc(
213
+ self,
214
+ need_bytes: int,
215
+ should_keep_waiting: Callable[[], bool] | None = None,
216
+ poll_s: float = 0.005,
217
+ ) -> bytearray:
218
+ """
219
+ Block until `need_bytes` of ADC data are available, then remove and
220
+ return exactly that many bytes from the front of the receive buffer.
221
+
222
+ Args:
223
+ need_bytes (int): Number of bytes to consume.
224
+ should_keep_waiting (Callable[[], bool] | None): Polled each iteration;
225
+ when it returns False, stop waiting and return what is available.
226
+ poll_s (float): Poll interval in seconds.
227
+
228
+ Returns:
229
+ bytearray: Up to `need_bytes` of ADC data (fewer only on early stop).
230
+
231
+ Raises:
232
+ AdcOverflowError: If the receive buffer exceeded its limit because
233
+ captured data was not read fast enough.
234
+ """
235
+ while len(self._adc_data_carry) < need_bytes:
236
+ if self._overflow_error is not None:
237
+ raise self._overflow_error
238
+ if should_keep_waiting is not None and not should_keep_waiting():
239
+ break
240
+ if not self.running and self._adc_data.empty():
241
+ break # stream stopped and no more data will arrive, return what we have
242
+ try:
243
+ chunk = self._adc_data.get(timeout=poll_s)
244
+ self._adc_data_carry.extend(chunk)
245
+ except queue.Empty:
246
+ continue # No new data yet, loop and check conditions again
247
+
248
+ if self._overflow_error is not None:
249
+ raise self._overflow_error
250
+ take_bytes_count = min(need_bytes, len(self._adc_data_carry))
251
+ out_bytes = self._adc_data_carry[:take_bytes_count]
252
+ del self._adc_data_carry[:take_bytes_count]
253
+ return out_bytes
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyQa40x-i2c
3
- Version: 0.1.2
3
+ Version: 0.2.0
4
4
  Summary: A Python library for the QA40x audio analyser (int2code fork)
5
5
  Maintainer-email: Hubert Stepniewski <hubert.stepniewski@int2code.com>
6
6
  License: MIT License
@@ -55,6 +55,8 @@ src/PyQa40x_i2c.egg-info/PKG-INFO
55
55
  src/PyQa40x_i2c.egg-info/SOURCES.txt
56
56
  src/PyQa40x_i2c.egg-info/dependency_links.txt
57
57
  src/PyQa40x_i2c.egg-info/requires.txt
58
+ src/PyQa40x_i2c.egg-info/scm_file_list.json
59
+ src/PyQa40x_i2c.egg-info/scm_version.json
58
60
  src/PyQa40x_i2c.egg-info/top_level.txt
59
61
  tests/__init__.py
60
62
  tests/test_template.py
@@ -0,0 +1,58 @@
1
+ {
2
+ "files": [
3
+ ".gitignore",
4
+ "README.md",
5
+ ".pylint_ignore",
6
+ "requirements-test.txt",
7
+ "requirements.txt",
8
+ "requirements-dev.txt",
9
+ "tox.ini",
10
+ "pyproject.toml",
11
+ "MANIFEST.in",
12
+ "RELEASE_NOTES.md",
13
+ "LICENSE",
14
+ "src/PyQa40x/wave.py",
15
+ "src/PyQa40x/math_general.py",
16
+ "src/PyQa40x/fft_processor.py",
17
+ "src/PyQa40x/wave_sine.py",
18
+ "src/PyQa40x/freq_series_plotter.py",
19
+ "src/PyQa40x/analyzer_params.py",
20
+ "src/PyQa40x/sig_proc.py",
21
+ "src/PyQa40x/control.py",
22
+ "src/PyQa40x/math_sines.py",
23
+ "src/PyQa40x/analyzer.py",
24
+ "src/PyQa40x/helpers.py",
25
+ "src/PyQa40x/registers.py",
26
+ "src/PyQa40x/cross_correlation.py",
27
+ "src/PyQa40x/series_plotter.py",
28
+ "src/PyQa40x/math_windows.py",
29
+ "src/PyQa40x/bluetooth.py",
30
+ "src/PyQa40x/math_energy.py",
31
+ "src/PyQa40x/math_chirp.py",
32
+ "src/PyQa40x/wave_chirp.py",
33
+ "src/PyQa40x/__init__.py",
34
+ "src/PyQa40x/stream.py",
35
+ "src/PyQa40x.egg-info/requires.txt",
36
+ "src/PyQa40x.egg-info/SOURCES.txt",
37
+ "src/PyQa40x.egg-info/top_level.txt",
38
+ "src/PyQa40x.egg-info/dependency_links.txt",
39
+ "src/PyQa40x.egg-info/PKG-INFO",
40
+ ".github/workflows/unit_tests.yml",
41
+ ".github/workflows/code_checker.yml",
42
+ ".github/workflows/publish_package.yml",
43
+ "_ci/is_dev_version.sh",
44
+ "LibTest/PyQa40x_cross_correlation.py",
45
+ "LibTest/PyQa40x_acoustic_chirp_spl_bt.py",
46
+ "LibTest/PyQa40x_acoustic_dbspl.py",
47
+ "LibTest/PyQa40x_freq_response_with_mask.py",
48
+ "LibTest/PyQa40x_pinknoise.py",
49
+ "LibTest/PyQa40x_acoustic_energy_decay_bt.py",
50
+ "LibTest/mask_phase.txt",
51
+ "LibTest/PyQa40x_coherent_averaging.py",
52
+ "LibTest/PyQa40x_thdn.py",
53
+ "LibTest/FirstPrinciples.py",
54
+ "LibTest/mask_freq.txt",
55
+ "tests/test_template.py",
56
+ "tests/__init__.py"
57
+ ]
58
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "tag": "0.2.0",
3
+ "distance": 0,
4
+ "node": "g6a5ba3357cf07c1d160ef5f122265823f2560ec9",
5
+ "dirty": false,
6
+ "branch": "HEAD",
7
+ "node_date": "2026-07-16"
8
+ }
@@ -1,208 +0,0 @@
1
- import threading
2
- import queue
3
- import time
4
- import usb1
5
- import numpy as np
6
-
7
-
8
- class Stream:
9
- """
10
- Class to manage buffer streaming, with event worker thread.
11
- """
12
-
13
- def __init__(self, context, device, registers):
14
- """
15
- Initializes the Stream class.
16
-
17
- Args:
18
- context (usb1.USBContext): The USB context.
19
- device (usb1.USBDeviceHandle): The USB device handle.
20
- registers (Registers): An instance of the Registers class.
21
- """
22
- self.context = context
23
- self.device = device
24
- self.registers = registers
25
- self.endpoint_read = usb1.ENDPOINT_IN | 0x02 # EP2 in
26
- self.endpoint_write = usb1.ENDPOINT_OUT | 0x02 # EP2 out
27
- self.dacQueue = queue.Queue(
28
- maxsize=5
29
- ) # Max. 5 overlapping buffers in flight, block on more
30
- self.adcQueue = queue.Queue() # Unlimited queue for received data buffers
31
- self.transfer_helper = usb1.USBTransferHelper() # Use the callback dispatcher
32
- # Handle COMPLETED transfers normally; also handle ERROR/TIMED_OUT/CANCELLED
33
- # so the queues are always drained and dacQueue.put() never deadlocks.
34
- for status in (
35
- usb1.TRANSFER_COMPLETED,
36
- usb1.TRANSFER_ERROR,
37
- usb1.TRANSFER_TIMED_OUT,
38
- usb1.TRANSFER_CANCELLED,
39
- usb1.TRANSFER_STALL,
40
- usb1.TRANSFER_NO_DEVICE,
41
- usb1.TRANSFER_OVERFLOW,
42
- ):
43
- self.transfer_helper.setEventCallback(status, self.callback)
44
- # self.received_data = bytearray() # Collection of received data bytes
45
- self.thread = None
46
- self.running = False
47
- self._pending_transfers = [] # track all submitted transfers for cancellation
48
- self._force_stop = (
49
- threading.Event()
50
- ) # raised to break the worker out of stuck queues
51
-
52
- def start(self):
53
- """
54
- Starts the streaming and spawns the worker thread.
55
- """
56
- self._force_stop.clear()
57
- self.thread = threading.Thread(target=self.worker, daemon=True)
58
- self.running = True
59
- self.thread.start()
60
- self.received_data = bytearray()
61
- self.registers.write(8, 0x05) # Start streaming
62
-
63
- def stop(self):
64
- """
65
- Stops the streaming and ends the worker thread.
66
-
67
- Sets ``running`` to False and cancels in-flight USB transfers so their
68
- callbacks fire and drain the queues, which allows the worker loop to
69
- exit naturally. If the worker has not exited within 5 seconds (e.g.
70
- because a USB cancellation is slow), ``_force_stop`` is set so the
71
- worker breaks unconditionally after its current
72
- ``handleEventsTimeout(0.1)`` call (≤ 100 ms), and we give it one
73
- further second to join before proceeding.
74
- """
75
- self.running = False
76
- # Cancel any in-flight transfers so their callbacks fire and drain the queues,
77
- # allowing the worker thread to exit its loop.
78
- for t in self._pending_transfers:
79
- try:
80
- t.cancel()
81
- except Exception:
82
- pass
83
- self._pending_transfers.clear()
84
- if self.thread is not None and self.thread.is_alive():
85
- self.thread.join(timeout=5.0)
86
- if self.thread.is_alive():
87
- # Worker is stuck (slow USB cancellation). Force it out so
88
- # context.close() does not deadlock.
89
- self._force_stop.set()
90
- self.thread.join(timeout=1.0)
91
- self.registers.write(8, 0x00) # Stop streaming
92
-
93
- def write(self, buffer):
94
- """
95
- Adds a buffer to the playback queue.
96
-
97
- Args:
98
- buffer (bytes): The data buffer to be written.
99
- """
100
- transfer = self.device.getTransfer()
101
- transfer.setBulk(self.endpoint_write, buffer, self.transfer_helper, None, 1000)
102
- transfer.submit() # Asynchronous transfer
103
- self._pending_transfers.append(transfer)
104
- self.dacQueue.put(transfer) # It doesn't matter what we put in here
105
-
106
- # Submit a USB bulk transfer to read
107
- read_transfer = self.device.getTransfer()
108
- read_transfer.setBulk(
109
- self.endpoint_read, 16384, self.transfer_helper, None, 1000
110
- )
111
- read_transfer.submit() # Asynchronous transfer
112
- self._pending_transfers.append(read_transfer)
113
- self.adcQueue.put(read_transfer) # It doesn't matter what we put in here
114
-
115
- def worker(self):
116
- """
117
- Event loop for the asynchronous transfers.
118
-
119
- Runs until *both* conditions are true:
120
- * ``running`` is False (``stop()`` has been called), and
121
- * both USB queues are empty (all callbacks have fired).
122
-
123
- An early exit is also triggered by ``_force_stop`` (set by ``stop()``
124
- when the normal join times out) so that ``context.close()`` is never
125
- called while this thread is still inside ``handleEventsTimeout``.
126
- """
127
- while self.running or not (
128
- self.dacQueue.empty() and self.adcQueue.empty()
129
- ): # Play until the last
130
- if self._force_stop.is_set():
131
- break
132
- self.context.handleEventsTimeout(
133
- 0.1
134
- ) # 100 ms timeout prevents blocking forever
135
-
136
- def callback(self, transfer):
137
- """
138
- Callback of the worker thread to handle completed transfers.
139
-
140
- Args:
141
- transfer (usb1.USBTransfer): The USB transfer that has completed.
142
- """
143
- if transfer.getEndpoint() == self.endpoint_read:
144
- if transfer.getStatus() == usb1.TRANSFER_COMPLETED:
145
- self.received_data.extend(
146
- transfer.getBuffer()
147
- ) # Collect received data bytes
148
- self.adcQueue.get_nowait() # Always drain the queue
149
- else:
150
- self.dacQueue.get_nowait() # Always drain the queue
151
-
152
- def collect_remaining_adc_data(self):
153
- """
154
- Waits for all remaining ADC transfers to complete and returns the collected data.
155
-
156
- Returns:
157
- bytearray: The collected ADC data.
158
- """
159
- while not self.adcQueue.empty():
160
- time.sleep(0.01)
161
-
162
- return self.received_data
163
-
164
- def write_zeros(self, chunk_bytes: int = 16384):
165
- """
166
- Sends a zero-filled DAC buffer of `chunk_bytes` bytes and queues one
167
- matching ADC read transfer. Functionally identical to write() but
168
- generates the zero payload internally so no DAC Wave is needed.
169
-
170
- Args:
171
- chunk_bytes (int): Number of bytes per transfer (must be a multiple
172
- of 4). Defaults to 16384 (16 kB) to match the hardware chunk
173
- size used by write().
174
- """
175
- zeros = bytes(chunk_bytes)
176
-
177
- dac_transfer = self.device.getTransfer()
178
- dac_transfer.setBulk(
179
- self.endpoint_write, zeros, self.transfer_helper, None, 1000
180
- )
181
- dac_transfer.submit()
182
- self._pending_transfers.append(dac_transfer)
183
- self.dacQueue.put(dac_transfer)
184
-
185
- read_transfer = self.device.getTransfer()
186
- read_transfer.setBulk(
187
- self.endpoint_read, chunk_bytes, self.transfer_helper, None, 1000
188
- )
189
- read_transfer.submit()
190
- self._pending_transfers.append(read_transfer)
191
- self.adcQueue.put(read_transfer)
192
-
193
- def collect_adc_exact(self, n_bytes: int) -> bytearray:
194
- """
195
- Blocks until at least `n_bytes` of ADC data have been collected, then
196
- returns exactly `n_bytes` (any excess is discarded). The stream must
197
- have been started with start() and all required write_zeros() calls
198
- must have been submitted before calling this.
199
-
200
- Args:
201
- n_bytes (int): Exact number of bytes to return.
202
-
203
- Returns:
204
- bytearray: Exactly n_bytes of calibrated ADC data.
205
- """
206
- while len(self.received_data) < n_bytes:
207
- time.sleep(0.01)
208
- return bytearray(self.received_data[:n_bytes])
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes