spectre-core 0.0.24__tar.gz → 0.0.26__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 (104) hide show
  1. {spectre_core-0.0.24 → spectre_core-0.0.26}/PKG-INFO +1 -1
  2. {spectre_core-0.0.24 → spectre_core-0.0.26}/pyproject.toml +1 -1
  3. {spectre_core-0.0.24 → spectre_core-0.0.26}/src/spectre_core/__init__.py +1 -1
  4. {spectre_core-0.0.24 → spectre_core-0.0.26}/src/spectre_core/capture_configs/_capture_config.py +2 -0
  5. {spectre_core-0.0.24 → spectre_core-0.0.26}/src/spectre_core/plotting/_panel_stack.py +11 -0
  6. {spectre_core-0.0.24 → spectre_core-0.0.26}/src/spectre_core/receivers/__init__.py +11 -8
  7. {spectre_core-0.0.24 → spectre_core-0.0.26}/src/spectre_core/receivers/_factory.py +16 -11
  8. spectre_core-0.0.26/src/spectre_core/receivers/_receiver.py +246 -0
  9. {spectre_core-0.0.24 → spectre_core-0.0.26}/src/spectre_core/receivers/_register.py +3 -3
  10. spectre_core-0.0.24/src/spectre_core/receivers/_spec_names.py → spectre_core-0.0.26/src/spectre_core/receivers/_specs.py +42 -7
  11. spectre_core-0.0.24/src/spectre_core/receivers/plugins/_usrp.py → spectre_core-0.0.26/src/spectre_core/receivers/plugins/_b200mini.py +87 -44
  12. spectre_core-0.0.24/src/spectre_core/receivers/plugins/gr/_usrp.py → spectre_core-0.0.26/src/spectre_core/receivers/plugins/_b200mini_gr.py +38 -61
  13. spectre_core-0.0.26/src/spectre_core/receivers/plugins/_custom.py +20 -0
  14. spectre_core-0.0.24/src/spectre_core/receivers/plugins/gr/_base.py → spectre_core-0.0.26/src/spectre_core/receivers/plugins/_gr.py +1 -1
  15. {spectre_core-0.0.24 → spectre_core-0.0.26}/src/spectre_core/receivers/plugins/_receiver_names.py +5 -3
  16. spectre_core-0.0.26/src/spectre_core/receivers/plugins/_rsp1a.py +64 -0
  17. spectre_core-0.0.26/src/spectre_core/receivers/plugins/_rsp1a_gr.py +112 -0
  18. spectre_core-0.0.26/src/spectre_core/receivers/plugins/_rspduo.py +76 -0
  19. spectre_core-0.0.26/src/spectre_core/receivers/plugins/_rspduo_gr.py +165 -0
  20. spectre_core-0.0.26/src/spectre_core/receivers/plugins/_sdrplay_receiver.py +262 -0
  21. spectre_core-0.0.26/src/spectre_core/receivers/plugins/_signal_generator.py +225 -0
  22. spectre_core-0.0.26/src/spectre_core/receivers/plugins/_signal_generator_gr.py +77 -0
  23. {spectre_core-0.0.24 → spectre_core-0.0.26}/src/spectre_core/spectrograms/_analytical.py +18 -18
  24. {spectre_core-0.0.24 → spectre_core-0.0.26}/src/spectre_core.egg-info/PKG-INFO +1 -1
  25. {spectre_core-0.0.24 → spectre_core-0.0.26}/src/spectre_core.egg-info/SOURCES.txt +9 -10
  26. spectre_core-0.0.26/tests/test_receivers.py +160 -0
  27. spectre_core-0.0.24/src/spectre_core/receivers/_base.py +0 -242
  28. spectre_core-0.0.24/src/spectre_core/receivers/plugins/_b200mini.py +0 -71
  29. spectre_core-0.0.24/src/spectre_core/receivers/plugins/_rsp1a.py +0 -69
  30. spectre_core-0.0.24/src/spectre_core/receivers/plugins/_rspduo.py +0 -86
  31. spectre_core-0.0.24/src/spectre_core/receivers/plugins/_sdrplay_receiver.py +0 -158
  32. spectre_core-0.0.24/src/spectre_core/receivers/plugins/_test.py +0 -225
  33. spectre_core-0.0.24/src/spectre_core/receivers/plugins/gr/__init__.py +0 -3
  34. spectre_core-0.0.24/src/spectre_core/receivers/plugins/gr/_rsp1a.py +0 -127
  35. spectre_core-0.0.24/src/spectre_core/receivers/plugins/gr/_rspduo.py +0 -202
  36. spectre_core-0.0.24/src/spectre_core/receivers/plugins/gr/_test.py +0 -117
  37. spectre_core-0.0.24/tests/test_receivers.py +0 -3
  38. {spectre_core-0.0.24 → spectre_core-0.0.26}/LICENSE +0 -0
  39. {spectre_core-0.0.24 → spectre_core-0.0.26}/README.md +0 -0
  40. {spectre_core-0.0.24 → spectre_core-0.0.26}/setup.cfg +0 -0
  41. {spectre_core-0.0.24 → spectre_core-0.0.26}/src/spectre_core/_file_io/__init__.py +0 -0
  42. {spectre_core-0.0.24 → spectre_core-0.0.26}/src/spectre_core/_file_io/file_handlers.py +0 -0
  43. {spectre_core-0.0.24 → spectre_core-0.0.26}/src/spectre_core/batches/__init__.py +0 -0
  44. {spectre_core-0.0.24 → spectre_core-0.0.26}/src/spectre_core/batches/_base.py +0 -0
  45. {spectre_core-0.0.24 → spectre_core-0.0.26}/src/spectre_core/batches/_batches.py +0 -0
  46. {spectre_core-0.0.24 → spectre_core-0.0.26}/src/spectre_core/batches/_factory.py +0 -0
  47. {spectre_core-0.0.24 → spectre_core-0.0.26}/src/spectre_core/batches/_register.py +0 -0
  48. {spectre_core-0.0.24 → spectre_core-0.0.26}/src/spectre_core/batches/plugins/_batch_keys.py +0 -0
  49. {spectre_core-0.0.24 → spectre_core-0.0.26}/src/spectre_core/batches/plugins/_callisto.py +0 -0
  50. {spectre_core-0.0.24 → spectre_core-0.0.26}/src/spectre_core/batches/plugins/_iq_stream.py +0 -0
  51. {spectre_core-0.0.24 → spectre_core-0.0.26}/src/spectre_core/capture_configs/__init__.py +0 -0
  52. {spectre_core-0.0.24 → spectre_core-0.0.26}/src/spectre_core/capture_configs/_capture_modes.py +0 -0
  53. {spectre_core-0.0.24 → spectre_core-0.0.26}/src/spectre_core/capture_configs/_capture_templates.py +0 -0
  54. {spectre_core-0.0.24 → spectre_core-0.0.26}/src/spectre_core/capture_configs/_parameters.py +0 -0
  55. {spectre_core-0.0.24 → spectre_core-0.0.26}/src/spectre_core/capture_configs/_pconstraints.py +0 -0
  56. {spectre_core-0.0.24 → spectre_core-0.0.26}/src/spectre_core/capture_configs/_pnames.py +0 -0
  57. {spectre_core-0.0.24 → spectre_core-0.0.26}/src/spectre_core/capture_configs/_ptemplates.py +0 -0
  58. {spectre_core-0.0.24 → spectre_core-0.0.26}/src/spectre_core/capture_configs/_pvalidators.py +0 -0
  59. {spectre_core-0.0.24 → spectre_core-0.0.26}/src/spectre_core/config/__init__.py +0 -0
  60. {spectre_core-0.0.24 → spectre_core-0.0.26}/src/spectre_core/config/_paths.py +0 -0
  61. {spectre_core-0.0.24 → spectre_core-0.0.26}/src/spectre_core/config/_time_formats.py +0 -0
  62. {spectre_core-0.0.24 → spectre_core-0.0.26}/src/spectre_core/exceptions.py +0 -0
  63. {spectre_core-0.0.24 → spectre_core-0.0.26}/src/spectre_core/jobs/__init__.py +0 -0
  64. {spectre_core-0.0.24 → spectre_core-0.0.26}/src/spectre_core/jobs/_duration.py +0 -0
  65. {spectre_core-0.0.24 → spectre_core-0.0.26}/src/spectre_core/jobs/_jobs.py +0 -0
  66. {spectre_core-0.0.24 → spectre_core-0.0.26}/src/spectre_core/jobs/_workers.py +0 -0
  67. {spectre_core-0.0.24 → spectre_core-0.0.26}/src/spectre_core/logs/__init__.py +0 -0
  68. {spectre_core-0.0.24 → spectre_core-0.0.26}/src/spectre_core/logs/_configure.py +0 -0
  69. {spectre_core-0.0.24 → spectre_core-0.0.26}/src/spectre_core/logs/_decorators.py +0 -0
  70. {spectre_core-0.0.24 → spectre_core-0.0.26}/src/spectre_core/logs/_logs.py +0 -0
  71. {spectre_core-0.0.24 → spectre_core-0.0.26}/src/spectre_core/logs/_process_types.py +0 -0
  72. {spectre_core-0.0.24 → spectre_core-0.0.26}/src/spectre_core/plotting/__init__.py +0 -0
  73. {spectre_core-0.0.24 → spectre_core-0.0.26}/src/spectre_core/plotting/_base.py +0 -0
  74. {spectre_core-0.0.24 → spectre_core-0.0.26}/src/spectre_core/plotting/_format.py +0 -0
  75. {spectre_core-0.0.24 → spectre_core-0.0.26}/src/spectre_core/plotting/_panel_names.py +0 -0
  76. {spectre_core-0.0.24 → spectre_core-0.0.26}/src/spectre_core/plotting/_panels.py +0 -0
  77. {spectre_core-0.0.24 → spectre_core-0.0.26}/src/spectre_core/post_processing/__init__.py +0 -0
  78. {spectre_core-0.0.24 → spectre_core-0.0.26}/src/spectre_core/post_processing/_base.py +0 -0
  79. {spectre_core-0.0.24 → spectre_core-0.0.26}/src/spectre_core/post_processing/_factory.py +0 -0
  80. {spectre_core-0.0.24 → spectre_core-0.0.26}/src/spectre_core/post_processing/_post_processor.py +0 -0
  81. {spectre_core-0.0.24 → spectre_core-0.0.26}/src/spectre_core/post_processing/_register.py +0 -0
  82. {spectre_core-0.0.24 → spectre_core-0.0.26}/src/spectre_core/post_processing/plugins/_event_handler_keys.py +0 -0
  83. {spectre_core-0.0.24 → spectre_core-0.0.26}/src/spectre_core/post_processing/plugins/_fixed_center_frequency.py +0 -0
  84. {spectre_core-0.0.24 → spectre_core-0.0.26}/src/spectre_core/post_processing/plugins/_swept_center_frequency.py +0 -0
  85. {spectre_core-0.0.24 → spectre_core-0.0.26}/src/spectre_core/py.typed +0 -0
  86. {spectre_core-0.0.24 → spectre_core-0.0.26}/src/spectre_core/receivers/plugins/__init__.py +0 -0
  87. {spectre_core-0.0.24 → spectre_core-0.0.26}/src/spectre_core/spectrograms/__init__.py +0 -0
  88. {spectre_core-0.0.24 → spectre_core-0.0.26}/src/spectre_core/spectrograms/_array_operations.py +0 -0
  89. {spectre_core-0.0.24 → spectre_core-0.0.26}/src/spectre_core/spectrograms/_spectrogram.py +0 -0
  90. {spectre_core-0.0.24 → spectre_core-0.0.26}/src/spectre_core/spectrograms/_transform.py +0 -0
  91. {spectre_core-0.0.24 → spectre_core-0.0.26}/src/spectre_core/wgetting/__init__.py +0 -0
  92. {spectre_core-0.0.24 → spectre_core-0.0.26}/src/spectre_core/wgetting/_callisto.py +0 -0
  93. {spectre_core-0.0.24 → spectre_core-0.0.26}/src/spectre_core.egg-info/dependency_links.txt +0 -0
  94. {spectre_core-0.0.24 → spectre_core-0.0.26}/src/spectre_core.egg-info/requires.txt +0 -0
  95. {spectre_core-0.0.24 → spectre_core-0.0.26}/src/spectre_core.egg-info/top_level.txt +0 -0
  96. {spectre_core-0.0.24 → spectre_core-0.0.26}/tests/test_batches.py +0 -0
  97. {spectre_core-0.0.24 → spectre_core-0.0.26}/tests/test_capture_configs.py +0 -0
  98. {spectre_core-0.0.24 → spectre_core-0.0.26}/tests/test_config.py +0 -0
  99. {spectre_core-0.0.24 → spectre_core-0.0.26}/tests/test_jobs.py +0 -0
  100. {spectre_core-0.0.24 → spectre_core-0.0.26}/tests/test_logs.py +0 -0
  101. {spectre_core-0.0.24 → spectre_core-0.0.26}/tests/test_plotting.py +0 -0
  102. {spectre_core-0.0.24 → spectre_core-0.0.26}/tests/test_post_processing.py +0 -0
  103. {spectre_core-0.0.24 → spectre_core-0.0.26}/tests/test_spectrograms.py +0 -0
  104. {spectre_core-0.0.24 → spectre_core-0.0.26}/tests/test_wgetting.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: spectre-core
3
- Version: 0.0.24
3
+ Version: 0.0.26
4
4
  Summary: The core Python package used by the spectre program.
5
5
  Maintainer-email: Jimmy Fitzpatrick <jcfitzpatrick12@gmail.com>
6
6
  License: GNU GENERAL PUBLIC LICENSE
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "spectre-core"
7
- version = "0.0.24"
7
+ version = "0.0.26"
8
8
  maintainers = [
9
9
  { name="Jimmy Fitzpatrick", email="jcfitzpatrick12@gmail.com" },
10
10
  ]
@@ -2,4 +2,4 @@
2
2
  # This file is part of SPECTRE
3
3
  # SPDX-License-Identifier: GPL-3.0-or-later
4
4
 
5
- __version__ = "0.0.24"
5
+ __version__ = "0.0.26"
@@ -49,6 +49,8 @@ class CaptureConfig(JsonHandler):
49
49
 
50
50
  Some substrings are reserved for third-party spectrogram data.
51
51
  """
52
+ if "_" in tag:
53
+ raise ValueError("An underscore is not allowed in a capture config tag.")
52
54
  if "callisto" in tag:
53
55
  raise ValueError(
54
56
  f"The substring `callisto` is reserved, and is not allowed in a capture config tag."
@@ -10,6 +10,7 @@ from matplotlib.figure import Figure
10
10
  from matplotlib.axes import Axes
11
11
  from matplotlib import use
12
12
  from datetime import datetime
13
+ import gc
13
14
 
14
15
  from spectre_core.spectrograms import TimeType
15
16
  from spectre_core.config import TimeFormat, get_batches_dir_path
@@ -281,10 +282,19 @@ class PanelStack:
281
282
 
282
283
  self._overlay_superimposed_panels()
283
284
 
285
+ def _close(self) -> None:
286
+ """Prevent memory leaks once a figure has been created, and successfully visualised."""
287
+ self._get_fig().clear()
288
+ plt.close(self._fig)
289
+ # Garbage collection seems to be required to prevent the memory leak.
290
+ # See https://github.com/jcfitzpatrick12/spectre/issues/128
291
+ gc.collect()
292
+
284
293
  def show(self) -> None:
285
294
  """Display the panel stack figure."""
286
295
  self._make_figure()
287
296
  self._get_fig().show()
297
+ self._close()
288
298
 
289
299
  def save(
290
300
  self,
@@ -310,4 +320,5 @@ class PanelStack:
310
320
  # If the parent directory does not exist, create it.
311
321
  os.makedirs(os.path.dirname(batch_file_path), exist_ok=True)
312
322
  self._get_fig().savefig(batch_file_path)
323
+ self._close()
313
324
  return batch_file_path
@@ -2,27 +2,30 @@
2
2
  # This file is part of SPECTRE
3
3
  # SPDX-License-Identifier: GPL-3.0-or-later
4
4
 
5
- """A vendor-neutral interface for collecting data from SDRs."""
5
+ """A vendor-neutral interface for capturing data from SDRs."""
6
6
 
7
7
  from .plugins._receiver_names import ReceiverName
8
- from .plugins._test import Test
8
+ from .plugins._signal_generator import SignalGenerator
9
9
  from .plugins._rsp1a import RSP1A
10
10
  from .plugins._rspduo import RSPduo
11
11
  from .plugins._b200mini import B200mini
12
12
 
13
- from ._base import BaseReceiver
13
+ from ._receiver import Receiver, ReceiverComponents
14
+ from ._specs import SpecName, Specs
14
15
  from ._factory import get_receiver
15
16
  from ._register import get_registered_receivers
16
- from ._spec_names import SpecName
17
17
 
18
18
  __all__ = [
19
- "Test",
19
+ "Receiver",
20
+ "ReceiverComponents",
21
+ "Specs",
22
+ "SpecName",
23
+ "ReceiverName",
24
+ "SignalGenerator",
20
25
  "RSP1A",
21
26
  "RSPduo",
22
27
  "B200mini",
23
- "BaseReceiver",
28
+ "Custom",
24
29
  "get_receiver",
25
30
  "get_registered_receivers",
26
- "SpecName",
27
- "ReceiverName",
28
31
  ]
@@ -2,16 +2,23 @@
2
2
  # This file is part of SPECTRE
3
3
  # SPDX-License-Identifier: GPL-3.0-or-later
4
4
 
5
- from typing import Optional, overload, Literal, TypeVar
5
+ from typing import Optional, overload, Literal
6
6
 
7
7
  from spectre_core.exceptions import ReceiverNotFoundError
8
8
  from ._register import receivers
9
- from ._base import BaseReceiver
9
+ from ._receiver import Receiver
10
10
  from .plugins._receiver_names import ReceiverName
11
+ from .plugins._signal_generator import SignalGenerator
11
12
  from .plugins._rsp1a import RSP1A
12
13
  from .plugins._rspduo import RSPduo
13
- from .plugins._test import Test
14
14
  from .plugins._b200mini import B200mini
15
+ from .plugins._custom import CustomReceiver
16
+
17
+
18
+ @overload
19
+ def get_receiver(
20
+ receiver_name: Literal[ReceiverName.SIGNAL_GENERATOR], mode: Optional[str] = None
21
+ ) -> SignalGenerator: ...
15
22
 
16
23
 
17
24
  @overload
@@ -28,25 +35,23 @@ def get_receiver(
28
35
 
29
36
  @overload
30
37
  def get_receiver(
31
- receiver_name: Literal[ReceiverName.TEST], mode: Optional[str] = None
32
- ) -> Test: ...
38
+ receiver_name: Literal[ReceiverName.B200MINI], mode: Optional[str] = None
39
+ ) -> B200mini: ...
33
40
 
34
41
 
35
42
  @overload
36
43
  def get_receiver(
37
- receiver_name: Literal[ReceiverName.B200MINI], mode: Optional[str] = None
38
- ) -> B200mini: ...
44
+ receiver_name: Literal[ReceiverName.CUSTOM], mode: Optional[str] = None
45
+ ) -> CustomReceiver: ...
39
46
 
40
47
 
41
48
  @overload
42
49
  def get_receiver(
43
50
  receiver_name: ReceiverName, mode: Optional[str] = None
44
- ) -> BaseReceiver: ...
51
+ ) -> Receiver: ...
45
52
 
46
53
 
47
- def get_receiver(
48
- receiver_name: ReceiverName, mode: Optional[str] = None
49
- ) -> BaseReceiver:
54
+ def get_receiver(receiver_name: ReceiverName, mode: Optional[str] = None) -> Receiver:
50
55
  """Get a registered receiver.
51
56
 
52
57
  :param receiver_name: The name of the receiver.
@@ -0,0 +1,246 @@
1
+ # SPDX-FileCopyrightText: © 2024-2025 Jimmy Fitzpatrick <jcfitzpatrick12@gmail.com>
2
+ # This file is part of SPECTRE
3
+ # SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ from typing import Callable, Optional, TypeVar, Generic, Any
6
+
7
+ from spectre_core.exceptions import ModeNotFoundError
8
+ from spectre_core.capture_configs import CaptureTemplate, Parameters, CaptureConfig
9
+ from .plugins._receiver_names import ReceiverName
10
+ from ._specs import Specs, SpecName
11
+
12
+ T = TypeVar("T")
13
+
14
+
15
+ class ReceiverComponents(Generic[T]):
16
+ """Base class for managing receiver components per operating mode."""
17
+
18
+ def __init__(self) -> None:
19
+ """Initialise an instance of `ReceiverComponents`."""
20
+ self._components: dict[str, T] = {}
21
+
22
+ @property
23
+ def modes(self) -> list[str]:
24
+ """Get all the added operating modes."""
25
+ return list(self._components.keys())
26
+
27
+ def add(self, mode: str, component: T) -> None:
28
+ """Add a component for a particular operating mode.
29
+
30
+ :param mode: The operating mode for the receiver.
31
+ :param component: The component associated with this mode.
32
+ """
33
+ self._components[mode] = component
34
+
35
+ def get(self, mode: str) -> T:
36
+ """Retrieve the component for a particular operating mode.
37
+
38
+ :param mode: The operating mode for the receiver.
39
+ :return: The component associated with this mode.
40
+ :raises ModeNotFoundError: If the mode is not found.
41
+ """
42
+ if mode not in self._components:
43
+ raise ModeNotFoundError(
44
+ f"Mode `{mode}` not found. Expected one of {self._components}"
45
+ )
46
+ return self._components[mode]
47
+
48
+
49
+ class CaptureMethods(ReceiverComponents[Callable[[str, Parameters], None]]):
50
+ """Per operating mode, define how the receiver captures data."""
51
+
52
+
53
+ class CaptureTemplates(ReceiverComponents[CaptureTemplate]):
54
+ """Per operating mode, define what parameters are required in a capture config, and the values each parameter can take."""
55
+
56
+
57
+ class PValidators(ReceiverComponents[Callable[[Parameters], None]]):
58
+ """Validate capture config parameters en groupe, per operating mode."""
59
+
60
+
61
+ def default_pvalidator(parameters: Parameters) -> None:
62
+ """A noop parameter validator. Doesn't check anything at all."""
63
+
64
+
65
+ class Receiver:
66
+ """An abstraction layer for software-defined radio receivers."""
67
+
68
+ def __init__(
69
+ self,
70
+ name: ReceiverName,
71
+ mode: Optional[str] = None,
72
+ specs: Optional[Specs] = None,
73
+ capture_methods: Optional[CaptureMethods] = None,
74
+ capture_templates: Optional[CaptureTemplates] = None,
75
+ pvalidators: Optional[PValidators] = None,
76
+ ) -> None:
77
+ """Initialise a receiver instance.
78
+
79
+ :param name: The name of the receiver.
80
+ :param capture_methods: Defines how the receiver captures data per mode.
81
+ :param capture_templates: Defines required parameters per mode.
82
+ :param pvalidators: Defines parameter validation functions per mode.
83
+ :param specs: Hardware specifications for the receiver.
84
+ :param mode: The initial active operating mode. Defaults to None.
85
+ """
86
+ self._name = name
87
+ self._mode = mode
88
+ self._specs = specs or Specs()
89
+ self._capture_methods = capture_methods or CaptureMethods()
90
+ self._capture_templates = capture_templates or CaptureTemplates()
91
+ self._pvalidators = pvalidators or PValidators()
92
+
93
+ @property
94
+ def name(self) -> ReceiverName:
95
+ """Retrieve the receiver's name."""
96
+ return self._name
97
+
98
+ @property
99
+ def mode(self) -> Optional[str]:
100
+ """Retrieve the operating mode."""
101
+ return self._mode
102
+
103
+ @mode.setter
104
+ def mode(self, value: str) -> None:
105
+ """Set the operating mode.
106
+
107
+ :param value: The new operating mode of the receiver. Use `None` to unset the mode.
108
+ """
109
+ if value not in self.modes:
110
+ raise ModeNotFoundError(
111
+ f"Mode `{value}` not found. Expected one of {self.modes}"
112
+ )
113
+ self._mode = value
114
+
115
+ @property
116
+ def modes(self) -> list[str]:
117
+ """The operating modes for the receiver.
118
+
119
+ :raises ValueError: If the modes are inconsistent.
120
+ """
121
+ if (
122
+ not self._capture_methods.modes
123
+ == self._pvalidators.modes
124
+ == self._capture_templates.modes
125
+ ):
126
+ raise ValueError(f"Modes are inconsistent for the receiver {self.name}.")
127
+ return self._capture_methods.modes
128
+
129
+ @property
130
+ def active_mode(self) -> str:
131
+ """Retrieve the active operating mode, raising an error if not set.
132
+
133
+ :raises ValueError: If no mode is currently set.
134
+ :return: The active operating mode.
135
+ """
136
+ if self._mode is None:
137
+ raise ValueError(
138
+ f"An active mode is not set for the receiver `{self.name.value}`. Currently, the mode is {self._mode}"
139
+ )
140
+ return self._mode
141
+
142
+ @property
143
+ def capture_method(self) -> Callable[[str, Parameters], None]:
144
+ """Retrieve the capture method for the active operating mode."""
145
+ return self._capture_methods.get(self.active_mode)
146
+
147
+ @property
148
+ def capture_template(self) -> CaptureTemplate:
149
+ """Retrieve the capture template for the active operating mode."""
150
+ return self._capture_templates.get(self.active_mode)
151
+
152
+ @property
153
+ def pvalidator(self) -> Callable[[Parameters], None]:
154
+ """Retrieve the parameter validator for the active operating mode."""
155
+ return self._pvalidators.get(self.active_mode)
156
+
157
+ @property
158
+ def specs(self) -> dict[SpecName, Any]:
159
+ """Retrieve all hardware specifications.
160
+
161
+ :return: A dictionary of all specifications.
162
+ """
163
+ return self._specs.all()
164
+
165
+ def start_capture(self, tag: str) -> None:
166
+ """Start capturing data using the active operating mode.
167
+
168
+ :param tag: The tag identifying the capture config.
169
+ :raises ValueError: If no mode is currently set.
170
+ """
171
+ self.capture_method(tag, self.load_parameters(tag))
172
+
173
+ def save_parameters(
174
+ self,
175
+ tag: str,
176
+ parameters: Parameters,
177
+ force: bool = False,
178
+ validate: bool = True,
179
+ ) -> None:
180
+ """Save parameters to a capture config.
181
+
182
+ :param tag: The tag identifying the capture config.
183
+ :param parameters: The parameters to save.
184
+ :param force: If True, overwrite existing configuration if it exists.
185
+ :param validate: If True, apply the capture template and pvalidator.
186
+ :raises ValueError: If no mode is currently set.
187
+ """
188
+ if validate:
189
+ parameters = self.capture_template.apply_template(parameters)
190
+ self.pvalidator(parameters)
191
+
192
+ capture_config = CaptureConfig(tag)
193
+ capture_config.save_parameters(
194
+ self.name.value, self.active_mode, parameters, force
195
+ )
196
+
197
+ def load_parameters(self, tag: str, validate: bool = True) -> Parameters:
198
+ """Load parameters from a capture config.
199
+
200
+ :param tag: The tag identifying the capture config.
201
+ :param validate: If True, apply the capture template and pvalidator.
202
+ :raises ValueError: If no mode is currently set.
203
+ :return: The validated parameters stored in the configuration.
204
+ """
205
+ capture_config = CaptureConfig(tag)
206
+
207
+ if validate:
208
+ parameters = self.capture_template.apply_template(capture_config.parameters)
209
+ self.pvalidator(parameters)
210
+ return parameters
211
+ else:
212
+ return capture_config.parameters
213
+
214
+ def add_mode(
215
+ self,
216
+ mode: str,
217
+ capture_method: Callable[[str, Parameters], None],
218
+ capture_template: CaptureTemplate,
219
+ pvalidator: Callable[[Parameters], None] = default_pvalidator,
220
+ ) -> None:
221
+ """Add a new mode to the receiver.
222
+
223
+ :param mode: The name of the new mode.
224
+ :param capture_method: Define how data is captured in this mode.
225
+ :param capture_template: Define what parameters are required in a capture config, and the values each parameter can take.
226
+ :param pvalidator: The function to validate parameters for this mode, as a group.
227
+ """
228
+ self._capture_methods.add(mode, capture_method)
229
+ self._capture_templates.add(mode, capture_template)
230
+ self._pvalidators.add(mode, pvalidator)
231
+
232
+ def add_spec(self, name: SpecName, value: Any) -> None:
233
+ """Add a hardware specification.
234
+
235
+ :param name: The specification's name.
236
+ :param value: The specification's value.
237
+ """
238
+ self._specs.add(name, value)
239
+
240
+ def get_spec(self, name: SpecName) -> Any:
241
+ """Retrieve a specific hardware specification.
242
+
243
+ :param name: The specification's name.
244
+ :return: The specification's value.
245
+ """
246
+ return self._specs.get(name)
@@ -5,12 +5,12 @@
5
5
  from typing import TypeVar, Callable, Type
6
6
 
7
7
  from .plugins._receiver_names import ReceiverName
8
- from ._base import BaseReceiver
8
+ from ._receiver import Receiver
9
9
 
10
10
  # map populated at runtime via the `register_receiver` decorator.
11
- receivers: dict[ReceiverName, Type[BaseReceiver]] = {}
11
+ receivers: dict[ReceiverName, Type[Receiver]] = {}
12
12
 
13
- T = TypeVar("T", bound=BaseReceiver)
13
+ T = TypeVar("T", bound=Receiver)
14
14
 
15
15
 
16
16
  def register_receiver(
@@ -2,11 +2,12 @@
2
2
  # This file is part of SPECTRE
3
3
  # SPDX-License-Identifier: GPL-3.0-or-later
4
4
 
5
+ from typing import Any
5
6
  from enum import Enum
6
7
 
7
8
 
8
9
  class SpecName(Enum):
9
- """A hardware specification name.
10
+ """Hardware specification names for SDR receivers.
10
11
 
11
12
  :ivar FREQUENCY_LOWER_BOUND: The lower bound for the center frequency, in Hz.
12
13
  :ivar FREQUENCY_UPPER_BOUND: The upper bound for the center frequency, in Hz.
@@ -16,15 +17,14 @@ class SpecName(Enum):
16
17
  :ivar BANDWIDTH_UPPER_BOUND: The upper bound for the bandwidth, in Hz.
17
18
  :ivar BANDWIDTH_OPTIONS: The permitted bandwidths for the receiver, in Hz.
18
19
  :ivar IF_GAIN_UPPER_BOUND: The upper bound for the intermediate frequency gain, in dB.
19
- Negative values indicate attenuation.
20
+ :ivar IF_GAIN_LOWER_BOUND: The lower bound for the intermediate frequency gain, in dB.
20
21
  :ivar RF_GAIN_UPPER_BOUND: The upper bound for the radio frequency gain, in dB.
21
- Negative values indicate attenuation.
22
+ :ivar RF_GAIN_LOWER_BOUND: The lower bound for the radio frequency gain, in dB.
22
23
  :ivar GAIN_UPPER_BOUND: The upper bound for the gain, in dB.
23
24
  :ivar WIRE_FORMATS: Supported data types transferred over the bus/network.
24
- :ivar MASTER_CLOCK_RATE_LOWER_BOUND: The lower bound for the SDR reference clock rate, in Hz.
25
- :ivar MASTER_CLOCK_RATE_UPPER_BOUND: The upper bound for the SDR reference clock rate, in Hz.
26
- :ivar API_RETUNING_LATENCY: An empirical estimate of the delay between issuing a command
27
- for a receiver to retune its center frequency and the actual physical update of the center frequency.
25
+ :ivar MASTER_CLOCK_RATE_LOWER_BOUND: The lower bound for the SDR reference clock rate, in Hz.
26
+ :ivar MASTER_CLOCK_RATE_UPPER_BOUND: The upper bound for the SDR reference clock rate, in Hz.
27
+ :ivar API_RETUNING_LATENCY: Estimated delay between issuing a retune command and the actual center frequency update.
28
28
  """
29
29
 
30
30
  FREQUENCY_LOWER_BOUND = "frequency_lower_bound"
@@ -35,9 +35,44 @@ class SpecName(Enum):
35
35
  BANDWIDTH_UPPER_BOUND = "bandwidth_upper_bound"
36
36
  BANDWIDTH_OPTIONS = "bandwidth_options"
37
37
  IF_GAIN_UPPER_BOUND = "if_gain_upper_bound"
38
+ IF_GAIN_LOWER_BOUND = "if_gain_lower_bound"
38
39
  RF_GAIN_UPPER_BOUND = "rf_gain_upper_bound"
40
+ RF_GAIN_LOWER_BOUND = "rf_gain_lower_bound"
39
41
  GAIN_UPPER_BOUND = "gain_upper_bound"
40
42
  WIRE_FORMATS = "wire_formats"
41
43
  MASTER_CLOCK_RATE_LOWER_BOUND = "master_clock_rate_lower_bound"
42
44
  MASTER_CLOCK_RATE_UPPER_BOUND = "master_clock_rate_upper_bound"
43
45
  API_RETUNING_LATENCY = "api_retuning_latency"
46
+
47
+
48
+ class Specs:
49
+ """Define hardware specifications."""
50
+
51
+ def __init__(self) -> None:
52
+ """Initialise an instance of `Specs`."""
53
+ self._specs: dict[SpecName, Any] = {}
54
+
55
+ def add(self, name: SpecName, value: Any) -> None:
56
+ """Add a hardware specification.
57
+
58
+ :param name: The specification's name.
59
+ :param value: The specification's value.
60
+ """
61
+ self._specs[name] = value
62
+
63
+ def get(self, name: SpecName) -> Any:
64
+ """Get a hardware specification.
65
+
66
+ :param name: The specification's name.
67
+ :return: The specification's value.
68
+ :raises KeyError: If the specification is not found.
69
+ """
70
+ if name not in self._specs:
71
+ raise KeyError(
72
+ f"Specification `{name}` not found. Expected one of {list(self._specs.keys())}"
73
+ )
74
+ return self._specs[name]
75
+
76
+ def all(self) -> dict[SpecName, Any]:
77
+ """Retrieve all hardware specifications."""
78
+ return self._specs