Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use thread lock for entire IQ capture method #48

Merged
merged 13 commits into from
Oct 23, 2023
Merged
8 changes: 4 additions & 4 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ default_language_version:
python: python3.8
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0
rev: v4.5.0
hooks:
- id: check-ast
types: [file, python]
Expand All @@ -18,7 +18,7 @@ repos:
- id: end-of-file-fixer
- id: trailing-whitespace
- repo: https://github.com/asottile/pyupgrade
rev: v3.10.1
rev: v3.15.0
hooks:
- id: pyupgrade
args: ["--py38-plus"]
Expand All @@ -30,12 +30,12 @@ repos:
types: [file, python]
args: ["--profile", "black", "--filter-files", "--gitignore"]
- repo: https://github.com/psf/black
rev: 23.7.0
rev: 23.10.0
hooks:
- id: black
types: [file, python]
- repo: https://github.com/igorshubovych/markdownlint-cli
rev: v0.35.0
rev: v0.37.0
hooks:
- id: markdownlint
types: [file, markdown]
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ classifiers = [
dependencies = [
"environs>=9.5.0",
"tekrsa-api-wrap>=1.3.2",
"scos_actions @ git+https://github.com/NTIA/scos-actions@6.3.3",
"scos_actions @ git+https://github.com/NTIA/scos-actions@6.4.1",
]

[project.optional-dependencies]
Expand Down
2 changes: 1 addition & 1 deletion src/scos_tekrsa/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "3.1.3"
__version__ = "3.1.4"
206 changes: 108 additions & 98 deletions src/scos_tekrsa/hardware/tekrsa_sigan.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
)

import scos_tekrsa.hardware.tekrsa_constants as rsa_constants
from scos_tekrsa import __version__ as SCOS_TEKRSA_VERSION
from scos_tekrsa import settings
from scos_tekrsa.hardware.mocks.rsa_block import MockRSA

Expand All @@ -21,6 +22,7 @@ def __init__(self):
try:
super().__init__()
logger.info("Initializing Tektronix RSA Signal Analyzer")
self._plugin_version = SCOS_TEKRSA_VERSION

self.rsa = None
self._is_available = False # should not be set outside of connect method
Expand Down Expand Up @@ -106,6 +108,11 @@ def is_available(self):
"""Returns True if initialized and ready for measurements"""
return self._is_available

@property
def plugin_version(self):
"""Returns the current version of scos-tekrsa."""
return self._plugin_version

@property
def sample_rate(self):
self._iq_bandwidth, self._sample_rate = self.rsa.IQSTREAM_GetAcqParameters()
Expand Down Expand Up @@ -255,109 +262,112 @@ def acquire_time_domain_samples(
cal_adjust: bool = True,
):
"""Acquire specific number of time-domain IQ samples."""
self._capture_time = None
if isinstance(num_samples, int) or (
isinstance(num_samples, float) and num_samples.is_integer()
):
nsamps_req = int(num_samples) # Requested number of samples
else:
raise ValueError("Requested number of samples must be an integer.")
nskip = int(num_samples_skip) # Requested number of samples to skip
nsamps = nsamps_req + nskip # Total number of samples to collect

if cal_adjust:
# Get calibration data for acquisition
if not (settings.RUNNING_TESTS or settings.MOCK_SIGAN):
cal_params = sensor_calibration.calibration_parameters
with sigan_lock:
self._capture_time = None
if isinstance(num_samples, int) or (
isinstance(num_samples, float) and num_samples.is_integer()
):
nsamps_req = int(num_samples) # Requested number of samples
else:
# Make it work for mock sigan/testing. Just match frequency.
cal_params = [vars(self)["_frequency"]]
try:
cal_args = [vars(self)[f"_{p}"] for p in cal_params]
except KeyError:
raise Exception(
"One or more required cal parameters is not a valid sigan setting."
)
logger.debug(f"Matched calibration params: {cal_args}")
self.recompute_sensor_calibration_data(cal_args)
# Compute the linear gain
db_gain = self.sensor_calibration_data["gain"]
linear_gain = 10.0 ** (db_gain / 20.0)
else:
linear_gain = 1
raise ValueError("Requested number of samples must be an integer.")
nskip = int(num_samples_skip) # Requested number of samples to skip
nsamps = nsamps_req + nskip # Total number of samples to collect

if cal_adjust:
# Get calibration data for acquisition
if not (settings.RUNNING_TESTS or settings.MOCK_SIGAN):
cal_params = sensor_calibration.calibration_parameters
else:
# Make it work for mock sigan/testing. Just match frequency.
cal_params = [vars(self)["_frequency"]]
try:
cal_args = [vars(self)[f"_{p}"] for p in cal_params]
except KeyError:
raise Exception(
"One or more required cal parameters is not a valid sigan setting."
)
logger.debug(f"Matched calibration params: {cal_args}")
self.recompute_sensor_calibration_data(cal_args)
# Compute the linear gain
db_gain = self.sensor_calibration_data["gain"]
linear_gain = 10.0 ** (db_gain / 20.0)
else:
linear_gain = 1

# Determine correct time length (round up, integer ms)
durationMsec = int(1000 * (nsamps / self.sample_rate)) + (
1000 * nsamps % self.sample_rate > 0
)
# Determine correct time length (round up, integer ms)
durationMsec = int(1000 * (nsamps / self.sample_rate)) + (
1000 * nsamps % self.sample_rate > 0
)

if durationMsec == 0:
# Num. samples requested is less than minimum duration for IQ stream.
# Handle this by skipping more samples than requested
durationMsec = 1 # Minimum allowed IQ stream duration
nskip = int((self.sample_rate / 1000) - nsamps_req)
nsamps = nskip + nsamps_req
if durationMsec == 0:
# Num. samples requested is less than minimum duration for IQ stream.
# Handle this by skipping more samples than requested
durationMsec = 1 # Minimum allowed IQ stream duration
nskip = int((self.sample_rate / 1000) - nsamps_req)
nsamps = nskip + nsamps_req

logger.debug(f"acquire_time_domain_samples starting, num_samples = {nsamps}")
logger.debug(f"Number of retries = {retries}")
logger.debug(
f"acquire_time_domain_samples starting, num_samples = {nsamps}"
)
logger.debug(f"Number of retries = {retries}")

max_retries = retries
max_retries = retries

while True:
self._capture_time = utils.get_datetime_str_now()
with sigan_lock:
while True:
self._capture_time = utils.get_datetime_str_now()
data, status = self.rsa.IQSTREAM_Tempfile_NoConfig(durationMsec, True)

data = data[nskip : nskip + nsamps_req] # Remove extra samples, if any
data_len = len(data)

logger.debug(f"IQ Stream status: {status}")

# Check status string for overload / data loss
self.overload = False
if "Input overrange" in status:
self.overload = True
logger.warning("IQ stream: ADC overrange event occurred.")

if "data loss" in status or "discontinuity" in status: # Invalid data
if retries > 0:
logger.warning(
f"Data loss occurred during IQ streaming. Retrying {retries} more times."
)
retries -= 1
continue
else:
err = "Data loss occurred with no retries remaining."
err += f" (tried {max_retries} times.)"
raise RuntimeError(err)
elif (
not data_len == nsamps_req
): # Invalid data: incorrect number of samples
if retries > 0:
msg = f"RSA error: requested {nsamps_req + nskip} samples, but got {data_len}."
logger.warning(msg)
logger.warning(f"Retrying {retries} more times.")
retries -= 1
continue
data = data[nskip : nskip + nsamps_req] # Remove extra samples, if any
data_len = len(data)

logger.debug(f"IQ Stream status: {status}")

# Check status string for overload / data loss
self.overload = False
if "Input overrange" in status:
self.overload = True
logger.warning("IQ stream: ADC overrange event occurred.")

if "data loss" in status or "discontinuity" in status: # Invalid data
if retries > 0:
logger.warning(
f"Data loss occurred during IQ streaming. Retrying {retries} more times."
)
retries -= 1
continue
else:
err = "Data loss occurred with no retries remaining."
err += f" (tried {max_retries} times.)"
raise RuntimeError(err)
elif (
not data_len == nsamps_req
): # Invalid data: incorrect number of samples
if retries > 0:
msg = f"RSA error: requested {nsamps_req + nskip} samples, but got {data_len}."
logger.warning(msg)
logger.warning(f"Retrying {retries} more times.")
retries -= 1
continue
else:
err = "Failed to acquire correct number of samples "
err += f"{max_retries} times in a row."
raise RuntimeError(err)
else:
err = "Failed to acquire correct number of samples "
err += f"{max_retries} times in a row."
raise RuntimeError(err)
else:
logger.debug(f"IQ stream: successfully acquired {data_len} samples.")
# Scale data to RF power and return
logger.debug(f"Applying gain of {linear_gain}")
data /= linear_gain

measurement_result = {
"data": data,
"overload": self.overload,
"frequency": self.frequency,
"reference_level": self.reference_level,
"sample_rate": self.rsa.IQSTREAM_GetAcqParameters()[1],
"capture_time": self._capture_time,
}
if self.device_name not in ["RSA306B", "RSA306"]:
measurement_result["attenuation"] = self.attenuation
measurement_result["preamp_enable"] = self.preamp_enable
return measurement_result
logger.debug(
f"IQ stream: successfully acquired {data_len} samples."
)
# Scale data to RF power and return
logger.debug(f"Applying gain of {linear_gain}")
data /= linear_gain

measurement_result = {
"data": data,
"overload": self.overload,
"frequency": self.frequency,
"reference_level": self.reference_level,
"sample_rate": self.rsa.IQSTREAM_GetAcqParameters()[1],
"capture_time": self._capture_time,
}
if self.device_name not in ["RSA306B", "RSA306"]:
measurement_result["attenuation"] = self.attenuation
measurement_result["preamp_enable"] = self.preamp_enable
return measurement_result