├── .gitignore ├── LICENSE ├── README.md ├── examples └── make_synthhd.py ├── setup.cfg ├── setup.py ├── tests ├── test_synthhd_base.py ├── test_synthhd_pro_v2.py ├── test_synthhd_v1p4.py └── test_synthhd_v2.py └── windfreak ├── __init__.py ├── device.py └── synth_hd.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Christian Hahn 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # windfreak-python [![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/christian-hahn/windfreak-python/blob/master/LICENSE) 2 | 3 | ## Abstract 4 | 5 | **windfreak** is a pure Python package to facilitate the use of [Windfreak Technologies](https://windfreaktech.com) devices. 6 | 7 | **windfreak** requires Python 3. 8 | 9 | **windfreak** is MIT licensed. 10 | 11 | ## Supported devices 12 | 13 | * SynthHD v1.4 14 | * SynthHD PRO v1.4 15 | * SynthHD v2 16 | * SynthHD PRO v2 17 | 18 | ## Installation 19 | 20 | ### Using `pip`: 21 | ```text 22 | pip install windfreak 23 | ``` 24 | 25 | ### Using `setup.py`: 26 | ```text 27 | git clone https://github.com/christian-hahn/windfreak-python.git 28 | cd windfreak-python 29 | python setup.py install 30 | ``` 31 | 32 | ### Using `conda`: 33 | Add `conda-forge` to your channels with 34 | ```text 35 | conda config --add channels conda-forge 36 | conda config --set channel_priority strict 37 | ``` 38 | 39 | then install the package with `conda`: 40 | ```text 41 | conda install windfreak 42 | ``` 43 | 44 | or with `mamba`: 45 | ```text 46 | mamba install windfreak 47 | ``` 48 | 49 | ## Example 50 | 51 | ### SynthHD 52 | 53 | ```python 54 | from windfreak import SynthHD 55 | 56 | synth = SynthHD('/dev/ttyACM0') 57 | synth.init() 58 | 59 | # Set channel 0 power and frequency 60 | synth[0].power = -10. 61 | synth[0].frequency = 2.e9 62 | 63 | # Enable channel 0 64 | synth[0].enable = True 65 | ``` 66 | 67 | ## License 68 | windfreak-python is covered under the MIT licensed. 69 | -------------------------------------------------------------------------------- /examples/make_synthhd.py: -------------------------------------------------------------------------------- 1 | from windfreak import SynthHD 2 | 3 | synth = SynthHD('/dev/ttyACM0') 4 | synth.init() 5 | 6 | # Set channel 0 power and frequency 7 | synth[0].power = -10. 8 | synth[0].frequency = 2.e9 9 | 10 | # Enable channel 0 output 11 | synth[0].enable = True -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | license_files = LICENSE.txt 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | from os import path 3 | 4 | here = path.abspath(path.dirname(__file__)) 5 | with open(path.join(here, 'README.md'), encoding='utf-8') as f: 6 | long_description = f.read() 7 | 8 | setup( 9 | name='windfreak', 10 | version='0.3.0', 11 | author='Christian Hahn', 12 | author_email='christianhahn09@gmail.com', 13 | description='Python package for Windfreak Technologies devices.', 14 | long_description=long_description, 15 | long_description_content_type='text/markdown', 16 | url='https://github.com/christian-hahn/windfreak-python', 17 | packages=find_packages(exclude=['tests', 'examples']), 18 | install_requires=[ 19 | 'pyserial', 20 | ], 21 | classifiers=[ 22 | 'Programming Language :: Python :: 3', 23 | 'License :: OSI Approved :: MIT License', 24 | 'Operating System :: OS Independent', 25 | ], 26 | license='MIT', 27 | python_requires='>=3', 28 | ) 29 | -------------------------------------------------------------------------------- /tests/test_synthhd_base.py: -------------------------------------------------------------------------------- 1 | """Base tests for SynthHD object. 2 | 3 | This module contains common unit-tests for the SynthHD object. 4 | """ 5 | 6 | from math import floor 7 | from time import sleep 8 | 9 | 10 | class SynthHDBaseTestCase: 11 | 12 | DEVPATH = '/dev/ttyACM0' 13 | NOMINAL_FREQUENCY = 1.e9 14 | NOMINAL_POWER = -10. 15 | 16 | ## Device 17 | 18 | def test_model_type(self): 19 | model = self._dut.model_type 20 | self.assertIsInstance(model, str) 21 | self.assertIn('WFT SynthHD', model) 22 | 23 | def test_serial_number(self): 24 | serial = self._dut.serial_number 25 | self.assertIsInstance(serial, int) 26 | self.assertGreaterEqual(serial, 0) 27 | 28 | def test_version(self): 29 | version = self._dut.firmware_version 30 | self.assertIsInstance(version, str) 31 | self.assertIn('Firmware Version', version) 32 | version = self._dut.hardware_version 33 | self.assertIsInstance(version, str) 34 | self.assertIn('Hardware Version', version) 35 | 36 | def test_save(self): 37 | self._dut.save() 38 | 39 | def test_reference_mode(self): 40 | modes = self._dut.reference_modes 41 | self.assertIsInstance(modes, tuple) 42 | for mode in modes: 43 | self.assertIsInstance(mode, str) 44 | self._dut.reference_mode = mode 45 | read = self._dut.reference_mode 46 | self.assertIsInstance(read, str) 47 | self.assertEqual(read, mode) 48 | 49 | def test_trigger_mode(self): 50 | modes = self._dut.trigger_modes 51 | self.assertIsInstance(modes, tuple) 52 | for mode in modes: 53 | self.assertIsInstance(mode, str) 54 | self._dut.trigger_mode = mode 55 | read = self._dut.trigger_mode 56 | self.assertIsInstance(read, str) 57 | self.assertEqual(read, mode) 58 | 59 | def test_temperature(self): 60 | value = self._dut.temperature 61 | self.assertIsInstance(value, float) 62 | 63 | def test_reference_frequency(self): 64 | self._dut.reference_mode = 'external' 65 | f_range = self._dut.reference_frequency_range 66 | self.assertIsInstance(f_range, dict) 67 | for key in ('start', 'stop', 'step'): 68 | self.assertIsInstance(f_range[key], float) 69 | self.assertLessEqual(f_range['start'], f_range['stop']) 70 | f_start = f_range['start'] 71 | f_stop = f_range['stop'] 72 | f_step = f_range['step'] 73 | coarse_step = (f_stop - f_start) / 17 74 | freq = f_start 75 | while freq <= f_stop: 76 | self._dut.reference_frequency = freq 77 | read = self._dut.reference_frequency 78 | self.assertIsInstance(read, float) 79 | error = abs(freq - read) 80 | print('Set reference frequency to {} Hz (error = {})' 81 | .format(freq, error)) 82 | self.assertLess(error, 0.1) 83 | freq = floor((freq + coarse_step - f_start) / f_step) * f_step + f_start 84 | 85 | ## Channel 86 | 87 | def test_frequency(self): 88 | for index, channel in enumerate(self._dut): 89 | others = [ch for ch in self._dut if not ch is channel] 90 | f_range = channel.frequency_range 91 | self.assertIsInstance(f_range, dict) 92 | for key in ('start', 'stop', 'step'): 93 | self.assertIsInstance(f_range[key], float) 94 | self.assertLessEqual(f_range['start'], f_range['stop']) 95 | f_start = f_range['start'] 96 | f_stop = f_range['stop'] 97 | f_step = f_range['step'] 98 | coarse_step = (f_stop - f_start) / 17 99 | freq = f_start 100 | while freq <= f_stop: 101 | before = [ch.frequency for ch in others] 102 | channel.frequency = freq 103 | read = channel.frequency 104 | after = [ch.frequency for ch in others] 105 | self.assertEqual(before, after) 106 | self.assertIsInstance(read, float) 107 | error = abs(freq - read) 108 | print('Set channel {} frequency to {} Hz (error = {})' 109 | .format(index, freq, error)) 110 | self.assertLess(error, 1.e-5) 111 | freq = floor((freq + coarse_step - f_start) / f_step) * f_step + f_start 112 | 113 | def test_power(self): 114 | for index, channel in enumerate(self._dut): 115 | others = [ch for ch in self._dut if not ch is channel] 116 | channel.frequency = self.NOMINAL_FREQUENCY 117 | p_range = channel.power_range 118 | self.assertIsInstance(p_range, dict) 119 | for key in ('start', 'stop', 'step'): 120 | self.assertIsInstance(p_range[key], float) 121 | self.assertLessEqual(p_range['start'], p_range['stop']) 122 | start = max(p_range['start'], -40.) 123 | stop = p_range['stop'] 124 | step = p_range['step'] 125 | coarse_step = (stop - start) / 17 126 | power = start 127 | while power <= stop: 128 | before = [ch.frequency for ch in others] 129 | channel.power = power 130 | read = channel.power 131 | after = [ch.frequency for ch in others] 132 | self.assertEqual(before, after) 133 | self.assertIsInstance(read, float) 134 | error = abs(power - read) 135 | print('Set channel {} power to {} dBm (error = {})' 136 | .format(index, power, error)) 137 | self.assertLess(error, 1.e-14) 138 | power = floor((power + coarse_step - start) / step) * step + start 139 | 140 | def test_calibrated(self): 141 | for channel in self._dut: 142 | channel.frequency = self.NOMINAL_FREQUENCY 143 | channel.power = self.NOMINAL_POWER 144 | channel.enable = True 145 | value = channel.calibrated 146 | self.assertIsInstance(value, bool) 147 | self.assertTrue(value) 148 | 149 | def test_temp_compensation_mode(self): 150 | for channel in self._dut: 151 | others = [ch for ch in self._dut if not ch is channel] 152 | modes = channel.temp_compensation_modes 153 | self.assertIsInstance(modes, tuple) 154 | for mode in modes: 155 | self.assertIsInstance(mode, str) 156 | before = [ch.temp_compensation_mode for ch in others] 157 | channel.temp_compensation_mode = mode 158 | read = channel.temp_compensation_mode 159 | after = [ch.temp_compensation_mode for ch in others] 160 | self.assertIsInstance(read, str) 161 | self.assertIn(read, modes) 162 | self.assertEqual(before, after) 163 | self.assertEqual(mode, read) 164 | 165 | def test_vga_dac(self): 166 | for channel in self._dut: 167 | channel.temp_compensation_mode = 'none' 168 | for index, channel in enumerate(self._dut): 169 | others = [ch for ch in self._dut if not ch is channel] 170 | dac_range = channel.vga_dac_range 171 | self.assertIsInstance(dac_range, dict) 172 | for key in ('start', 'stop', 'step'): 173 | self.assertIsInstance(dac_range[key], int) 174 | self.assertLessEqual(dac_range['start'], dac_range['stop']) 175 | start = dac_range['start'] 176 | stop = dac_range['stop'] 177 | step = dac_range['step'] 178 | coarse_step = (stop - start) / 33 179 | value = start 180 | while value <= stop: 181 | before = [ch.vga_dac for ch in others] 182 | channel.vga_dac = value 183 | read = channel.vga_dac 184 | after = [ch.vga_dac for ch in others] 185 | self.assertEqual(before, after) 186 | self.assertIsInstance(read, int) 187 | print('Set channel {} VGA DAC to {} (read = {})' 188 | .format(index, value, read)) 189 | self.assertEqual(value, read) 190 | value = floor((value + coarse_step - start) / step) * step + start 191 | 192 | def test_phase(self): 193 | for index, channel in enumerate(self._dut): 194 | others = [ch for ch in self._dut if not ch is channel] 195 | p_range = channel.phase_range 196 | self.assertIsInstance(p_range, dict) 197 | for key in ('start', 'stop', 'step'): 198 | self.assertIsInstance(p_range[key], float) 199 | self.assertLessEqual(p_range['start'], p_range['stop']) 200 | start = p_range['start'] 201 | stop = p_range['stop'] 202 | step = p_range['step'] 203 | coarse_step = (stop - start) / 33 204 | phase = start 205 | while phase <= stop: 206 | before = [ch.frequency for ch in others] 207 | channel.phase = phase 208 | read = channel.phase 209 | after = [ch.frequency for ch in others] 210 | self.assertEqual(before, after) 211 | self.assertIsInstance(read, float) 212 | error = abs(phase - read) 213 | print('Set channel {} phase to {} degrees (error = {})' 214 | .format(index, phase, error)) 215 | self.assertLess(error, .006) 216 | phase = floor((phase + coarse_step - start) / step) * step + start 217 | 218 | def channel_enable_helper(self, attr): 219 | for channel in self._dut: 220 | others = [ch for ch in self._dut if not ch is channel] 221 | for value in (False, True, False): 222 | before = [getattr(ch, attr) for ch in others] 223 | setattr(channel, attr, value) 224 | read = getattr(channel, attr) 225 | after = [getattr(ch, attr) for ch in others] 226 | self.assertIsInstance(read, bool) 227 | self.assertEqual(before, after) 228 | self.assertEqual(value, read) 229 | 230 | def test_rf_enable(self): 231 | self.channel_enable_helper('rf_enable') 232 | 233 | def test_pa_enable(self): 234 | self.channel_enable_helper('pa_enable') 235 | 236 | def test_pll_enable(self): 237 | self.channel_enable_helper('pll_enable') 238 | 239 | def test_enable(self): 240 | self.channel_enable_helper('enable') 241 | 242 | def test_lock_status(self): 243 | for channel in self._dut: 244 | channel.frequency = self.NOMINAL_FREQUENCY 245 | channel.power = self.NOMINAL_POWER 246 | channel.enable = True 247 | sleep(1.) 248 | value = channel.lock_status 249 | self.assertIsInstance(value, bool) 250 | self.assertTrue(value) 251 | 252 | def test_modulation_enables(self): 253 | enables = ('sweep_enable', 'am_enable', 'pulse_mod_enable', 254 | 'dual_pulse_mod_enable', 'fm_enable') 255 | for enable in enables: 256 | others = [en for en in enables if not en == enable] 257 | for value in (False, True, False): 258 | before = [getattr(self._dut, en) for en in others] 259 | setattr(self._dut, enable, value) 260 | read = getattr(self._dut, enable) 261 | after = [getattr(self._dut, en) for en in others] 262 | self.assertIsInstance(read, bool) 263 | self.assertEqual(before, after) 264 | self.assertEqual(value, read) 265 | 266 | 267 | class SynthHDv2BaseTestCase(SynthHDBaseTestCase): 268 | 269 | def test_channel_spacing(self): 270 | for index, channel in enumerate(self._dut): 271 | others = [ch for ch in self._dut if not ch is channel] 272 | cs_range = channel.channel_spacing_range 273 | self.assertIsInstance(cs_range, dict) 274 | for key in ('start', 'stop', 'step'): 275 | self.assertIsInstance(cs_range[key], float) 276 | self.assertLessEqual(cs_range['start'], cs_range['stop']) 277 | f_start = cs_range['start'] 278 | f_stop = cs_range['stop'] 279 | f_step = cs_range['step'] 280 | coarse_step = (f_stop - f_start) / 13 281 | freq = f_start 282 | while freq <= f_stop: 283 | before = [ch.channel_spacing for ch in others] 284 | channel.channel_spacing = freq 285 | read = channel.channel_spacing 286 | after = [ch.channel_spacing for ch in others] 287 | self.assertEqual(before, after) 288 | self.assertIsInstance(read, float) 289 | error = abs(freq - read) 290 | print('Set channel {} channel spacing to {} Hz (error = {})' 291 | .format(index, freq, error)) 292 | self.assertLess(error, 1.e-10) 293 | freq = floor((freq + coarse_step - f_start) / f_step) * f_step + f_start 294 | -------------------------------------------------------------------------------- /tests/test_synthhd_pro_v2.py: -------------------------------------------------------------------------------- 1 | """Tests for SynthHD object. 2 | 3 | This module contains unit-tests for the SynthHD object specific to SynthHD PRO v2. 4 | """ 5 | 6 | import sys 7 | import unittest 8 | from test_synthhd_base import SynthHDv2BaseTestCase 9 | from windfreak import SynthHD 10 | 11 | 12 | class SynthHDPROv2TestCase(unittest.TestCase, SynthHDv2BaseTestCase): 13 | 14 | def setUp(self): 15 | self._dut = SynthHD(self.DEVPATH) 16 | self._dut.init() 17 | 18 | def tearDown(self): 19 | self._dut.init() 20 | self._dut.close() 21 | del self._dut 22 | 23 | def test_model(self): 24 | model = self._dut.model 25 | self.assertIsInstance(model, str) 26 | self.assertEqual(model, 'SynthHD PRO v2') 27 | 28 | def test_frequency_range(self): 29 | expected = {'start': 10.e6, 'stop': 24000.e6, 'step': 0.1} 30 | for channel in self._dut: 31 | f_range = channel.frequency_range 32 | self.assertIsInstance(f_range, dict) 33 | self.assertEqual(f_range, expected) 34 | 35 | def test_power_range(self): 36 | expected = {'start': -70., 'stop': 20., 'step': 0.01} 37 | for channel in self._dut: 38 | p_range = channel.power_range 39 | self.assertIsInstance(p_range, dict) 40 | self.assertEqual(p_range, expected) 41 | 42 | def test_vga_dac_range(self): 43 | expected = {'start': 0, 'stop': 4000, 'step': 1} 44 | for channel in self._dut: 45 | vga_range = channel.vga_dac_range 46 | self.assertIsInstance(vga_range, dict) 47 | self.assertEqual(vga_range, expected) 48 | 49 | 50 | if __name__ == '__main__': 51 | if len(sys.argv) > 1: 52 | SynthHDPROv2TestCase.DEVPATH = sys.argv.pop() 53 | unittest.main() 54 | -------------------------------------------------------------------------------- /tests/test_synthhd_v1p4.py: -------------------------------------------------------------------------------- 1 | """Tests for SynthHD object. 2 | 3 | This module contains unit-tests for the SynthHD object specific to SynthHD v1.4. 4 | """ 5 | 6 | import sys 7 | import unittest 8 | from test_synthhd_base import SynthHDBaseTestCase 9 | from windfreak import SynthHD 10 | 11 | 12 | class SynthHDv1p4TestCase(unittest.TestCase, SynthHDBaseTestCase): 13 | 14 | def setUp(self): 15 | self._dut = SynthHD(self.DEVPATH) 16 | self._dut.init() 17 | 18 | def tearDown(self): 19 | self._dut.init() 20 | self._dut.close() 21 | del self._dut 22 | 23 | def test_model(self): 24 | model = self._dut.model 25 | self.assertIsInstance(model, str) 26 | self.assertEqual(model, 'SynthHD v1.4') 27 | 28 | def test_frequency_range(self): 29 | expected = {'start': 53.e6, 'stop': 13999.999999e6, 'step': 0.1} 30 | for channel in self._dut: 31 | f_range = channel.frequency_range 32 | self.assertIsInstance(f_range, dict) 33 | self.assertEqual(f_range, expected) 34 | 35 | def test_power_range(self): 36 | expected = {'start': -80., 'stop': 20., 'step': 0.01} 37 | for channel in self._dut: 38 | p_range = channel.power_range 39 | self.assertIsInstance(p_range, dict) 40 | self.assertEqual(p_range, expected) 41 | 42 | def test_vga_dac_range(self): 43 | expected = {'start': 0, 'stop': 45000, 'step': 1} 44 | for channel in self._dut: 45 | vga_range = channel.vga_dac_range 46 | self.assertIsInstance(vga_range, dict) 47 | self.assertEqual(vga_range, expected) 48 | 49 | 50 | if __name__ == '__main__': 51 | if len(sys.argv) > 1: 52 | SynthHDv1p4TestCase.DEVPATH = sys.argv.pop() 53 | unittest.main() 54 | -------------------------------------------------------------------------------- /tests/test_synthhd_v2.py: -------------------------------------------------------------------------------- 1 | """Tests for SynthHD object. 2 | 3 | This module contains unit-tests for the SynthHD object specific to SynthHD v2. 4 | """ 5 | 6 | import sys 7 | import unittest 8 | from test_synthhd_base import SynthHDv2BaseTestCase 9 | from windfreak import SynthHD 10 | 11 | 12 | class SynthHDv2TestCase(unittest.TestCase, SynthHDv2BaseTestCase): 13 | 14 | def setUp(self): 15 | self._dut = SynthHD(self.DEVPATH) 16 | self._dut.init() 17 | 18 | def tearDown(self): 19 | self._dut.init() 20 | self._dut.close() 21 | del self._dut 22 | 23 | def test_model(self): 24 | model = self._dut.model 25 | self.assertIsInstance(model, str) 26 | self.assertEqual(model, 'SynthHD v2') 27 | 28 | def test_frequency_range(self): 29 | expected = {'start': 10.e6, 'stop': 15000.e6, 'step': 0.1} 30 | for channel in self._dut: 31 | f_range = channel.frequency_range 32 | self.assertIsInstance(f_range, dict) 33 | self.assertEqual(f_range, expected) 34 | 35 | def test_power_range(self): 36 | expected = {'start': -70., 'stop': 20., 'step': 0.01} 37 | for channel in self._dut: 38 | p_range = channel.power_range 39 | self.assertIsInstance(p_range, dict) 40 | self.assertEqual(p_range, expected) 41 | 42 | def test_vga_dac_range(self): 43 | expected = {'start': 0, 'stop': 4000, 'step': 1} 44 | for channel in self._dut: 45 | vga_range = channel.vga_dac_range 46 | self.assertIsInstance(vga_range, dict) 47 | self.assertEqual(vga_range, expected) 48 | 49 | 50 | if __name__ == '__main__': 51 | if len(sys.argv) > 1: 52 | SynthHDv2TestCase.DEVPATH = sys.argv.pop() 53 | unittest.main() 54 | -------------------------------------------------------------------------------- /windfreak/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = '0.3.0' 2 | 3 | from .synth_hd import SynthHD -------------------------------------------------------------------------------- /windfreak/device.py: -------------------------------------------------------------------------------- 1 | from serial import Serial 2 | 3 | 4 | class SerialDevice: 5 | 6 | def __init__(self, devpath): 7 | self._devpath = devpath 8 | self._dev = None 9 | self.open() 10 | 11 | def __del__(self): 12 | self.close() 13 | 14 | def open(self): 15 | if self._dev is not None: 16 | raise RuntimeError('Device has already been opened.') 17 | self._dev = Serial(port=self._devpath, timeout=10) 18 | 19 | def close(self): 20 | if self._dev is not None: 21 | self._dev.close() 22 | self._dev = None 23 | 24 | def write(self, attribute, *args): 25 | dtype, request, _ = self.API[attribute] 26 | dtype = dtype if isinstance(dtype, tuple) else (dtype,) 27 | if len(args) != len(dtype): 28 | raise ValueError('Number of arguments and data-types are not equal.') 29 | args = ((int(ar) if dt is bool else dt(ar)) for dt, ar in zip(dtype, args)) 30 | self._write(request.format(*args)) 31 | 32 | def read(self, attribute, *args): 33 | dtype, _, request = self.API[attribute] 34 | dtype = dtype if isinstance(dtype, tuple) else (dtype,) 35 | if len(args) + 1 != len(dtype): 36 | raise ValueError('Must have +1 more data-type than argument.') 37 | args = ((int(ar) if dt is bool else dt(ar)) for dt, ar in zip(dtype, args)) 38 | ret = self._query(request.format(*args)) 39 | dtype = dtype[-1] 40 | if dtype is bool: 41 | ret = int(ret) 42 | if ret not in (0, 1): 43 | raise ValueError('Invalid return value \'{}\' for type bool.'.format(ret)) 44 | return dtype(ret) 45 | 46 | def _write(self, data): 47 | """Write to device. 48 | 49 | Args: 50 | data (str): write data 51 | """ 52 | self._dev.write(data.encode('utf-8')) 53 | 54 | def _read(self): 55 | """Read from device. 56 | 57 | Returns: 58 | str: data 59 | """ 60 | rdata = self._dev.readline() 61 | if not rdata.endswith(b'\n'): 62 | raise TimeoutError('Expected newline terminator.') 63 | return rdata.decode('utf-8').strip() 64 | 65 | def _query(self, data): 66 | """Write to device and read response. 67 | 68 | Args: 69 | data (str): write data 70 | 71 | Returns: 72 | str: data 73 | """ 74 | self._write(data) 75 | return self._read() 76 | -------------------------------------------------------------------------------- /windfreak/synth_hd.py: -------------------------------------------------------------------------------- 1 | from .device import SerialDevice 2 | from collections.abc import Sequence 3 | 4 | 5 | class SynthHDChannel: 6 | 7 | def __init__(self, parent, index): 8 | self._parent = parent 9 | self._index = index 10 | model = self._parent.model 11 | if model == 'SynthHD v1.4': 12 | self._f_range = {'start': 53.e6, 'stop': 13999.999999e6, 'step': 0.1} 13 | self._p_range = {'start': -80., 'stop': 20., 'step': 0.01} 14 | self._vga_range = {'start': 0, 'stop': 45000, 'step': 1} 15 | else: 16 | self._f_range = None 17 | self._p_range = None 18 | self._vga_range = None 19 | 20 | def init(self): 21 | """Initialize device.""" 22 | self.enable = False 23 | f_range = self.frequency_range 24 | if f_range is not None: 25 | self.frequency = f_range['start'] 26 | p_range = self.power_range 27 | if p_range is not None: 28 | self.power = p_range['start'] 29 | self.phase = 0. 30 | self.temp_compensation_mode = '10 sec' 31 | 32 | def write(self, attribute, *args): 33 | self.select() 34 | self._parent.write(attribute, *args) 35 | 36 | def read(self, attribute, *args): 37 | self.select() 38 | return self._parent.read(attribute, *args) 39 | 40 | def select(self): 41 | """Select channel.""" 42 | self._parent.write('channel', self._index) 43 | 44 | @property 45 | def frequency_range(self): 46 | """Frequency range in Hz. 47 | 48 | Returns: 49 | dict: frequency range or None 50 | """ 51 | return None if self._f_range is None else self._f_range.copy() 52 | 53 | @property 54 | def frequency(self): 55 | """Get frequency in Hz. 56 | 57 | Returns: 58 | float: frequency in Hz 59 | """ 60 | return self.read('frequency') * 1e6 61 | 62 | @frequency.setter 63 | def frequency(self, value): 64 | """Set frequency in Hz. 65 | 66 | Args: 67 | value (float / int): frequency in Hz 68 | """ 69 | if not isinstance(value, (float, int)): 70 | raise ValueError('Expected float or int.') 71 | f_range = self.frequency_range 72 | if f_range is not None and not f_range['start'] <= value <= f_range['stop']: 73 | raise ValueError('Expected float in range [{}, {}] Hz.'.format( 74 | f_range['start'], f_range['stop'])) 75 | self.write('frequency', value / 1e6) 76 | 77 | @property 78 | def power_range(self): 79 | """Power range in dBm. 80 | 81 | Returns: 82 | dict: power range or None 83 | """ 84 | return None if self._p_range is None else self._p_range.copy() 85 | 86 | @property 87 | def power(self): 88 | """Get power in dBm. 89 | 90 | Returns: 91 | float: power in dBm 92 | """ 93 | return self.read('power') 94 | 95 | @power.setter 96 | def power(self, value): 97 | """Set power in dBm. 98 | 99 | Args: 100 | value (float / int): power in dBm 101 | """ 102 | if not isinstance(value, (float, int)): 103 | raise TypeError('Expected float or int.') 104 | self.write('power', value) 105 | 106 | @property 107 | def calibrated(self): 108 | """Calibration was successful on frequency or amplitude change. 109 | 110 | Returns: 111 | bool: calibrated 112 | """ 113 | return self.read('calibrated') 114 | 115 | @property 116 | def temp_compensation_modes(self): 117 | """Temperature compensation modes. 118 | 119 | Returns: 120 | tuple: tuple of str of modes 121 | """ 122 | return ('none', 'on set', '1 sec', '10 sec') 123 | 124 | @property 125 | def temp_compensation_mode(self): 126 | """Temperature compensation mode. 127 | 128 | Returns: 129 | str: mode 130 | """ 131 | return self.temp_compensation_modes[self.read('temp_comp_mode')] 132 | 133 | @temp_compensation_mode.setter 134 | def temp_compensation_mode(self, value): 135 | modes = self.temp_compensation_modes 136 | if not value in modes: 137 | raise ValueError('Expected str in set {}.'.format(modes)) 138 | self.write('temp_comp_mode', modes.index(value)) 139 | 140 | @property 141 | def vga_dac_range(self): 142 | """VGA DAC value range. 143 | 144 | Returns: 145 | dict: VGA DAC range or None 146 | """ 147 | return None if self._vga_range is None else self._vga_range.copy() 148 | 149 | @property 150 | def vga_dac(self): 151 | """Get raw VGA DAC value 152 | 153 | Returns: 154 | int: value 155 | """ 156 | return self.read('vga_dac') 157 | 158 | @vga_dac.setter 159 | def vga_dac(self, value): 160 | """Set raw VGA DAC value. 161 | 162 | Args: 163 | value (int): value 164 | """ 165 | if not isinstance(value, int): 166 | raise TypeError('Expected int.') 167 | self.write('vga_dac', value) 168 | 169 | @property 170 | def phase_range(self): 171 | """Phase step range. 172 | 173 | Returns: 174 | dict: range 175 | """ 176 | return { 177 | 'start': 0., 178 | 'stop': 360., 179 | 'step': .001, 180 | } 181 | 182 | @property 183 | def phase(self): 184 | """Get phase step value. 185 | 186 | Returns: 187 | float: value in degrees 188 | """ 189 | return self.read('phase_step') 190 | 191 | @phase.setter 192 | def phase(self, value): 193 | """Set phase step value. 194 | 195 | Args: 196 | value (float / int): phase in degrees 197 | """ 198 | if not isinstance(value, (float, int)): 199 | raise TypeError('Expected float or int.') 200 | self.write('phase_step', value) 201 | 202 | @property 203 | def rf_enable(self): 204 | """RF output enable. 205 | 206 | Returns: 207 | bool: enable 208 | """ 209 | return self.read('rf_enable') 210 | 211 | @rf_enable.setter 212 | def rf_enable(self, value): 213 | if not isinstance(value, bool): 214 | raise ValueError('Expected bool.') 215 | self.write('rf_enable', value) 216 | 217 | @property 218 | def pa_enable(self): 219 | """PA enable. 220 | 221 | Returns: 222 | bool: enable 223 | """ 224 | return self.read('pa_power_on') 225 | 226 | @pa_enable.setter 227 | def pa_enable(self, value): 228 | if not isinstance(value, bool): 229 | raise ValueError('Expected bool.') 230 | self.write('pa_power_on', value) 231 | 232 | @property 233 | def pll_enable(self): 234 | """PLL enable. 235 | 236 | Returns: 237 | bool: enable 238 | """ 239 | return self.read('pll_power_on') 240 | 241 | @pll_enable.setter 242 | def pll_enable(self, value): 243 | if not isinstance(value, bool): 244 | raise ValueError('Expected bool.') 245 | self.write('pll_power_on', value) 246 | 247 | @property 248 | def enable(self): 249 | """Get output enable. 250 | 251 | Returns: 252 | bool: enabled 253 | """ 254 | return self.rf_enable and self.pll_enable and self.pa_enable 255 | 256 | @enable.setter 257 | def enable(self, value): 258 | """Set output enable. 259 | 260 | Args: 261 | value (bool): enable 262 | """ 263 | if not isinstance(value, bool): 264 | raise TypeError('Expected bool.') 265 | self.rf_enable = value 266 | self.pll_enable = value 267 | self.pa_enable = value 268 | 269 | @property 270 | def lock_status(self): 271 | """PLL lock status. 272 | 273 | Returns: 274 | bool: locked 275 | """ 276 | return self.read('pll_lock') 277 | 278 | 279 | class SynthHDv2Channel(SynthHDChannel): 280 | 281 | def __init__(self, parent, index): 282 | self._parent = parent 283 | self._index = index 284 | model = self._parent.model 285 | if model == 'SynthHD v2': 286 | self._f_range = {'start': 10.e6, 'stop': 15000.e6, 'step': 0.1} 287 | self._p_range = {'start': -70., 'stop': 20., 'step': 0.01} 288 | self._vga_range = {'start': 0, 'stop': 4000, 'step': 1} 289 | self._cspacing_range = {'start': 0.1, 'stop': 1000., 'step': 0.1} 290 | elif model == 'SynthHD PRO v2': 291 | self._f_range = {'start': 10.e6, 'stop': 24000.e6, 'step': 0.1} 292 | self._p_range = {'start': -70., 'stop': 20., 'step': 0.01} 293 | self._vga_range = {'start': 0, 'stop': 4000, 'step': 1} 294 | self._cspacing_range = {'start': 0.1, 'stop': 1000., 'step': 0.1} 295 | else: 296 | self._f_range = None 297 | self._p_range = None 298 | self._vga_range = None 299 | self._cspacing_range = None 300 | 301 | @property 302 | def channel_spacing_range(self): 303 | """Channel Spacing Range in Hz. 304 | 305 | Returns: 306 | dict: channel spacing range or None 307 | """ 308 | return None if self._cspacing_range is None else self._cspacing_range.copy() 309 | 310 | @property 311 | def channel_spacing(self): 312 | """Channel Spacing in Hz 313 | 314 | Returns: 315 | float: Channel Spacing setting in Hz 316 | """ 317 | return self.read('channelspacing') 318 | 319 | @channel_spacing.setter 320 | def channel_spacing(self,value): 321 | """Set Channel Spacing in Hz. 322 | 323 | Args: 324 | float: Channel spacing in Hz 325 | """ 326 | if not isinstance(value, (float, int)): 327 | raise ValueError('Expected float or int.') 328 | cs_range = self.channel_spacing_range 329 | if cs_range is not None and not cs_range['start'] <= value <= cs_range['stop']: 330 | raise ValueError('Expected float in range [{}, {}] Hz.'.format( 331 | cs_range['start'], cs_range['stop'])) 332 | self.write('channelspacing', value) 333 | 334 | 335 | class SynthHD(SerialDevice, Sequence): 336 | 337 | API = { 338 | # name type write read 339 | 'channel': (int, 'C{}', 'C?'), # Select channel 340 | 'frequency': (float, 'f{:.8f}', 'f?'), # Frequency in MHz 341 | 'power': (float, 'W{:.3f}', 'W?'), # Power in dBm 342 | 'calibrated': (bool, None, 'V'), 343 | 'temp_comp_mode': (int, 'Z{}', 'Z?'), 344 | 'vga_dac': (int, 'a{}', 'a?'), # VGA DAC value [0, 45000] 345 | 'phase_step': (float, '~{:.3f}', '~?'), # Phase step in degrees 346 | 'rf_enable': (bool, 'h{}', 'h?'), 347 | 'pa_power_on': (bool, 'r{}', 'r?'), 348 | 'pll_power_on': (bool, 'E{}', 'E?'), 349 | 'model_type': (str, None, '+'), # Model type 350 | 'serial_number': (int, None, '-'), # Serial number 351 | 'fw_version': (str, None, 'v0'), # Firmware version 352 | 'hw_version': (str, None, 'v1'), # Hardware version 353 | 'sub_version': (str, None, 'v2'), # Sub-version: "HD" or "HDPRO". Only Synth HD >= v2. 354 | 'save': ((), 'e', None), # Program all settings to EEPROM 355 | 'reference_mode': (int, 'x{}', 'x?'), 356 | 'trig_function': (int, 'w{}', 'w?'), 357 | 'pll_lock': (bool, None, 'p'), 358 | 'temperature': (float, None, 'z'), # Temperature in Celsius 359 | 'ref_frequency': (float, '*{:.8f}', '*?'), # Reference frequency in MHz 360 | 'channelspacing': (float, 'i{:.1f}', 'i?'), # Channel spacing in Hz 361 | 362 | 'sweep_freq_low': (float, 'l{:.8f}', 'l?'), # Sweep lower frequency in MHz 363 | 'sweep_freq_high': (float, 'u{:.8f}', 'u?'), # Sweep upper frequency in MHz 364 | 'sweep_freq_step': (float, 's{:.8f}', 's?'), # Sweep frequency step in MHz 365 | 'sweep_time_step': (float, 't{:.3f}', 't?'), # Sweep time step in [4, 10000] ms 366 | 'sweep_power_low': (float, '[{:.3f}', '[?'), # Sweep lower power [-60, +20] dBm 367 | 'sweep_power_high': (float, ']{:.3f}', ']?'), # Sweep upper power [-60, +20] dBm 368 | 'sweep_direction': (int, '^{}', '^?'), # Sweep direction 369 | 'sweep_diff_freq': (float, 'k{:.8f}', 'k?'), # Sweep differential frequency in MHz 370 | 'sweep_diff_meth': (int, 'n{}', 'n?'), # Sweep differential method 371 | 'sweep_type': (int, 'X{}', 'X?'), # Sweep type {0: linear, 1: tabular} 372 | 'sweep_single': (bool, 'g{}', 'g?'), 373 | 'sweep_cont': (bool, 'c{}', 'c?'), 374 | 375 | 'am_time_step': (int, 'F{}', 'F?'), # Time step in microseconds 376 | 'am_num_samples': (int, 'q{}', 'q?'), # Number of samples in one burst 377 | 'am_cont': (bool, 'A{}', 'A?'), # Enable continuous AM modulation 378 | 'am_lookup_table': ((int, float), '@{}a{:.3f}', '@{}a?'), # Program row in lookup table in dBm 379 | 380 | 'pulse_on_time': (int, 'P{}', 'P?'), # Pulse on time in range [1, 10e6] us 381 | 'pulse_off_time': (int, 'O{}', 'O?'), # Pulse off time in range [2, 10e6] uS 382 | 'pulse_num_rep': (int, 'R{}', 'R?'), # Number of repetitions in range [1, 65500] 383 | 'pulse_invert': (bool, ':{}', ':?'), # Invert pulse polarity 384 | 'pulse_single': ((), 'G', None), 385 | 'pulse_cont': (bool, 'j{}', 'j?'), 386 | 'dual_pulse_mod': (bool, 'D{}', 'D?'), 387 | 388 | 'fm_frequency': (int, '<{}', '{}', '>?'), 390 | 'fm_num_samples': (int, ',{}', ',?'), 391 | 'fm_mod_type': (int, ';{}', ';?'), 392 | 'fm_cont': (bool, '/{}', '/?'), 393 | } 394 | 395 | def __init__(self, devpath): 396 | super().__init__(devpath) 397 | self._model = None 398 | self._model = self.model 399 | if 'v2' in self.model: 400 | channel_type = SynthHDv2Channel 401 | else: 402 | channel_type = SynthHDChannel 403 | self._channels = [channel_type(self, index) for index in range(2)] 404 | 405 | def __getitem__(self, key): 406 | return self._channels.__getitem__(key) 407 | 408 | def __len__(self): 409 | return self._channels.__len__() 410 | 411 | def init(self): 412 | """Initialize device: put into a known, safe state.""" 413 | self.reference_mode = 'internal 27mhz' 414 | self.trigger_mode = 'disabled' 415 | self.sweep_enable = False 416 | self.am_enable = False 417 | self.pulse_mod_enable = False 418 | self.dual_pulse_mod_enable = False 419 | self.fm_enable = False 420 | for channel in self: 421 | channel.init() 422 | 423 | @property 424 | def model(self): 425 | """Model version. This is the binned version that dictates API support. 426 | 427 | Returns: 428 | str: model version or None if unsupported 429 | """ 430 | if self._model is not None: 431 | return self._model 432 | hw_ver = self.hardware_version 433 | if 'Version 2.' in hw_ver: 434 | sub_ver = self.read('sub_version') 435 | if sub_ver == 'HD': 436 | return 'SynthHD v2' 437 | elif sub_ver == 'HDPRO': 438 | return 'SynthHD PRO v2' 439 | else: 440 | # Unsupported sub-version. Return None. 441 | return None 442 | elif 'Version 1.4' in hw_ver: 443 | return 'SynthHD v1.4' 444 | else: 445 | # Unsupported hardware version. Return None. 446 | return None 447 | 448 | @property 449 | def model_type(self): 450 | """Model type. 451 | 452 | Returns: 453 | str: model 454 | """ 455 | return self.read('model_type') 456 | 457 | @property 458 | def serial_number(self): 459 | """Serial number 460 | 461 | Returns: 462 | int: serial number 463 | """ 464 | return self.read('serial_number') 465 | 466 | @property 467 | def firmware_version(self): 468 | """Firmware version. 469 | 470 | Returns: 471 | str: version 472 | """ 473 | return self.read('fw_version') 474 | 475 | @property 476 | def hardware_version(self): 477 | """Hardware version. 478 | 479 | Returns: 480 | str: version 481 | """ 482 | return self.read('hw_version') 483 | 484 | def save(self): 485 | """Save all settings to non-volatile EEPROM.""" 486 | self.write('save') 487 | 488 | @property 489 | def reference_modes(self): 490 | """Frequency reference modes. 491 | 492 | Returns: 493 | tuple: tuple of str of modes 494 | """ 495 | return ('external', 'internal 27mhz', 'internal 10mhz') 496 | 497 | @property 498 | def reference_mode(self): 499 | """Get frequency reference mode. 500 | 501 | Returns: 502 | str: mode 503 | """ 504 | return self.reference_modes[self.read('reference_mode')] 505 | 506 | @reference_mode.setter 507 | def reference_mode(self, value): 508 | """Set frequency reference mode. 509 | 510 | Args: 511 | value (str): mode 512 | """ 513 | modes = self.reference_modes 514 | if not value in modes: 515 | raise ValueError('Expected str in set {}.'.format(modes)) 516 | self.write('reference_mode', modes.index(value)) 517 | 518 | @property 519 | def trigger_modes(self): 520 | """Trigger modes. 521 | 522 | Returns: 523 | tuple: tuple of str of modes 524 | """ 525 | return ( 526 | 'disabled', 527 | 'full frequency sweep', 528 | 'single frequency step', 529 | 'stop all', 530 | 'rf enable', 531 | 'remove interrupts', 532 | 'reserved', 533 | 'reserved', 534 | 'am modulation', 535 | 'fm modulation', 536 | ) 537 | 538 | @property 539 | def trigger_mode(self): 540 | """Get trigger mode. 541 | 542 | Returns: 543 | str: mode 544 | """ 545 | return self.trigger_modes[self.read('trig_function')] 546 | 547 | @trigger_mode.setter 548 | def trigger_mode(self, value): 549 | """Set trigger mode. 550 | 551 | Args: 552 | value (str): mode 553 | """ 554 | modes = self.trigger_modes 555 | if not value in modes: 556 | raise ValueError('Expected str in set {}.'.format(modes)) 557 | self.write('trig_function', modes.index(value)) 558 | 559 | @property 560 | def temperature(self): 561 | """Temperature in Celsius. 562 | 563 | Returns: 564 | float: temperature 565 | """ 566 | return self.read('temperature') 567 | 568 | @property 569 | def reference_frequency_range(self): 570 | """Reference frequency range in Hz. 571 | 572 | Returns: 573 | dict: frequency range in Hz 574 | """ 575 | return {'start': 10.e6, 'stop': 100.e6, 'step': 1.e3} 576 | 577 | @property 578 | def reference_frequency(self): 579 | """Get reference frequency in Hz. 580 | 581 | Returns: 582 | float: frequency in Hz 583 | """ 584 | return self.read('ref_frequency') * 1.e6 585 | 586 | @reference_frequency.setter 587 | def reference_frequency(self, value): 588 | """Set reference frequency in Hz. 589 | 590 | Args: 591 | value (float / int): frequency in Hz 592 | """ 593 | if not isinstance(value, (float, int)): 594 | raise ValueError('Expected float or int.') 595 | f_range = self.reference_frequency_range 596 | if not f_range['start'] <= value <= f_range['stop']: 597 | raise ValueError('Expected float in range [{}, {}] Hz.'.format( 598 | f_range['start'], f_range['stop'])) 599 | self.write('ref_frequency', value / 1.e6) 600 | 601 | @property 602 | def sweep_enable(self): 603 | """Get sweep continuously enable. 604 | 605 | Returns: 606 | bool: enable 607 | """ 608 | return self.read('sweep_cont') 609 | 610 | @sweep_enable.setter 611 | def sweep_enable(self, value): 612 | """Set sweep continuously enable. 613 | 614 | Args: 615 | value (bool): enable 616 | """ 617 | if not isinstance(value, bool): 618 | raise ValueError('Expected bool.') 619 | self.write('sweep_cont', value) 620 | 621 | @property 622 | def am_enable(self): 623 | """Get AM continuously enable. 624 | 625 | Returns: 626 | bool: enable 627 | """ 628 | return self.read('am_cont') 629 | 630 | @am_enable.setter 631 | def am_enable(self, value): 632 | """Set AM continuously enable. 633 | 634 | Args: 635 | value (bool): enable 636 | """ 637 | if not isinstance(value, bool): 638 | raise ValueError('Expected bool.') 639 | self.write('am_cont', value) 640 | 641 | @property 642 | def pulse_mod_enable(self): 643 | """Get pulse modulation continuously enable. 644 | 645 | Returns: 646 | bool: enable 647 | """ 648 | return self.read('pulse_cont') 649 | 650 | @pulse_mod_enable.setter 651 | def pulse_mod_enable(self, value): 652 | """Set pulse modulation continuously enable. 653 | 654 | Args: 655 | value (bool): enable 656 | """ 657 | if not isinstance(value, bool): 658 | raise ValueError('Expected bool.') 659 | self.write('pulse_cont', value) 660 | 661 | @property 662 | def dual_pulse_mod_enable(self): 663 | """Get dual pulse modulation enable. 664 | 665 | Returns: 666 | bool: enable 667 | """ 668 | return self.read('dual_pulse_mod') 669 | 670 | @dual_pulse_mod_enable.setter 671 | def dual_pulse_mod_enable(self, value): 672 | """Set dual pulse modulation enable. 673 | 674 | Args: 675 | value (bool): enable 676 | """ 677 | if not isinstance(value, bool): 678 | raise ValueError('Expected bool.') 679 | self.write('dual_pulse_mod', value) 680 | 681 | @property 682 | def fm_enable(self): 683 | """Get FM continuously enable. 684 | 685 | Returns: 686 | bool: enable 687 | """ 688 | return self.read('fm_cont') 689 | 690 | @fm_enable.setter 691 | def fm_enable(self, value): 692 | """Set FM continuously enable. 693 | 694 | Args: 695 | value (bool): enable 696 | """ 697 | if not isinstance(value, bool): 698 | raise ValueError('Expected bool.') 699 | self.write('fm_cont', value) 700 | --------------------------------------------------------------------------------