pypsgctrl 0.1.1__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.
- pypsgctrl-0.1.1/Doxyfile +12 -0
- pypsgctrl-0.1.1/LICENSE +21 -0
- pypsgctrl-0.1.1/MANIFEST.in +5 -0
- pypsgctrl-0.1.1/PKG-INFO +177 -0
- pypsgctrl-0.1.1/docs/api_reference_en.md +377 -0
- pypsgctrl-0.1.1/docs/api_reference_ru.md +378 -0
- pypsgctrl-0.1.1/pyproject.toml +30 -0
- pypsgctrl-0.1.1/pypsgctrl/__init__.py +16 -0
- pypsgctrl-0.1.1/pypsgctrl/channel.py +184 -0
- pypsgctrl-0.1.1/pypsgctrl/device.py +252 -0
- pypsgctrl-0.1.1/pypsgctrl/enums.py +60 -0
- pypsgctrl-0.1.1/pypsgctrl/errors.py +17 -0
- pypsgctrl-0.1.1/pypsgctrl/registers.py +121 -0
- pypsgctrl-0.1.1/pypsgctrl/transport.py +18 -0
- pypsgctrl-0.1.1/pypsgctrl.egg-info/PKG-INFO +177 -0
- pypsgctrl-0.1.1/pypsgctrl.egg-info/SOURCES.txt +20 -0
- pypsgctrl-0.1.1/pypsgctrl.egg-info/dependency_links.txt +1 -0
- pypsgctrl-0.1.1/pypsgctrl.egg-info/requires.txt +1 -0
- pypsgctrl-0.1.1/pypsgctrl.egg-info/top_level.txt +1 -0
- pypsgctrl-0.1.1/readme.md +157 -0
- pypsgctrl-0.1.1/setup.cfg +4 -0
- pypsgctrl-0.1.1/tests/test_pypsgctrl.py +146 -0
pypsgctrl-0.1.1/Doxyfile
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
PROJECT_NAME = pypsgctrl
|
|
2
|
+
PROJECT_NUMBER = 0.1.1
|
|
3
|
+
OUTPUT_DIRECTORY = build/doxygen
|
|
4
|
+
INPUT = pypsgctrl readme.md docs/api_reference_en.md docs/api_reference_ru.md
|
|
5
|
+
FILE_PATTERNS = *.py *.md
|
|
6
|
+
RECURSIVE = YES
|
|
7
|
+
OPTIMIZE_OUTPUT_JAVA = YES
|
|
8
|
+
EXTRACT_ALL = YES
|
|
9
|
+
GENERATE_HTML = YES
|
|
10
|
+
GENERATE_LATEX = NO
|
|
11
|
+
QUIET = YES
|
|
12
|
+
MARKDOWN_ID_STYLE = GITHUB
|
pypsgctrl-0.1.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Oleg Kochetov
|
|
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.
|
pypsgctrl-0.1.1/PKG-INFO
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pypsgctrl
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Python driver for the PSG9080 dual-channel function generator with USB serial control.
|
|
5
|
+
Author: Oleg Kochetov
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Repository, https://github.com/realspinner/pypsgctrl
|
|
8
|
+
Project-URL: Documentation, https://github.com/realspinner/pypsgctrl/blob/main/docs/api_reference_en.md
|
|
9
|
+
Project-URL: Issues, https://github.com/realspinner/pypsgctrl/issues
|
|
10
|
+
Keywords: PSG9080,function-generator,serial,instrument-control
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Topic :: Scientific/Engineering
|
|
15
|
+
Requires-Python: >=3.10
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Requires-Dist: pyserial>=3.5
|
|
19
|
+
Dynamic: license-file
|
|
20
|
+
|
|
21
|
+
# pypsgctrl
|
|
22
|
+
|
|
23
|
+
English documentation is followed by the Russian translation.
|
|
24
|
+
После английского текста приведена русская версия.
|
|
25
|
+
|
|
26
|
+
## English
|
|
27
|
+
|
|
28
|
+
A synchronous Python driver for the PSG9080 dual-channel function generator over
|
|
29
|
+
USB serial. Supports waveforms, frequency, amplitude, offset, duty cycle, phase,
|
|
30
|
+
modulation, measurement, sweep, system settings, and parameter memory.
|
|
31
|
+
|
|
32
|
+
### Installation
|
|
33
|
+
|
|
34
|
+
Python 3.10 or newer is required. pyserial is installed automatically.
|
|
35
|
+
|
|
36
|
+
For releases published on PyPI:
|
|
37
|
+
|
|
38
|
+
```sh
|
|
39
|
+
python -m pip install pypsgctrl
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The package is available on TestPyPI; production publication is pending.
|
|
43
|
+
Until then, install directly from GitHub:
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
python -m pip install "git+https://github.com/realspinner/pypsgctrl.git"
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
To install this release from TestPyPI in a fresh virtual environment:
|
|
50
|
+
|
|
51
|
+
```sh
|
|
52
|
+
python -m pip install pyserial
|
|
53
|
+
python -m pip install --index-url https://test.pypi.org/simple/ --no-deps pypsgctrl==0.1.1
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
After cloning the repository, use `python -m pip install -e .` for development. Serial settings: 115200 baud,
|
|
57
|
+
8-N-1, without flow control. Replace the macOS example port with your device's port.
|
|
58
|
+
Opening a connection does not enable outputs.
|
|
59
|
+
|
|
60
|
+
### Quick start
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
from pypsgctrl import PSG9080, Waveform
|
|
64
|
+
|
|
65
|
+
with PSG9080.connect('/dev/cu.usbserial-2120', timeout=1.0) as generator:
|
|
66
|
+
print(generator.ch1.frequency) # Decimal, Hz
|
|
67
|
+
generator.ch1.waveform = Waveform.SINE
|
|
68
|
+
generator.ch1.frequency = 1000
|
|
69
|
+
generator.ch1.amplitude = '0.100' # Vpp, not RMS
|
|
70
|
+
generator.ch1.offset = 0 # V
|
|
71
|
+
generator.set_outputs(True, False)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Writes take effect immediately. Physical readbacks use Decimal; numeric strings
|
|
75
|
+
and Decimal support exact fractions. Values between device steps are rejected.
|
|
76
|
+
Transactions are serialized with an instance lock. After a timeout, close and
|
|
77
|
+
reopen the connection. Hardware limits absent from the protocol remain the caller's responsibility.
|
|
78
|
+
|
|
79
|
+
### Documentation and structure
|
|
80
|
+
|
|
81
|
+
[English API reference](https://github.com/realspinner/pypsgctrl/blob/main/docs/api_reference_en.md) · [Russian API reference](https://github.com/realspinner/pypsgctrl/blob/main/docs/api_reference_ru.md)
|
|
82
|
+
|
|
83
|
+
| Module | Purpose |
|
|
84
|
+
| --- | --- |
|
|
85
|
+
| `pypsgctrl/__init__.py` | Stable public imports and version |
|
|
86
|
+
| `pypsgctrl/device.py` | Connection, wire exchange, general settings |
|
|
87
|
+
| `pypsgctrl/channel.py` | Channel properties, frequency/output control |
|
|
88
|
+
| `pypsgctrl/registers.py` | Register metadata and unit conversion |
|
|
89
|
+
| `pypsgctrl/enums.py` | Waveform, frequency, modulation, trigger enums |
|
|
90
|
+
| `pypsgctrl/errors.py` | Driver exceptions |
|
|
91
|
+
| `pypsgctrl/transport.py` | Injectable transport contract |
|
|
92
|
+
|
|
93
|
+
Public imports use `from pypsgctrl import PSG9080, Waveform`.
|
|
94
|
+
Report bugs through [GitHub Issues](https://github.com/realspinner/pypsgctrl/issues). Source comments use Doxygen
|
|
95
|
+
tags. With Doxygen installed, run `doxygen Doxyfile` from the repository root to generate
|
|
96
|
+
`build/doxygen/html`.
|
|
97
|
+
|
|
98
|
+
### License
|
|
99
|
+
|
|
100
|
+
MIT License, copyright 2026 Oleg Kochetov. See [LICENSE](https://github.com/realspinner/pypsgctrl/blob/main/LICENSE).
|
|
101
|
+
Vendor PDFs and recorder calibration utilities are not included in this repository.
|
|
102
|
+
|
|
103
|
+
## Русский
|
|
104
|
+
|
|
105
|
+
Синхронный Python-драйвер двухканального генератора PSG9080 по USB serial.
|
|
106
|
+
Поддерживает форму, частоту, амплитуду, смещение, duty, фазу, модуляцию,
|
|
107
|
+
измерение, sweep, системные настройки и память параметров.
|
|
108
|
+
|
|
109
|
+
### Установка
|
|
110
|
+
|
|
111
|
+
Требуется Python 3.10 или новее. pyserial устанавливается автоматически.
|
|
112
|
+
|
|
113
|
+
Для релизов, опубликованных на основном PyPI:
|
|
114
|
+
|
|
115
|
+
```sh
|
|
116
|
+
python -m pip install pypsgctrl
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Пакет доступен на TestPyPI; публикация на основном PyPI ещё не выполнена.
|
|
120
|
+
До неё можно установить пакет из GitHub:
|
|
121
|
+
|
|
122
|
+
```sh
|
|
123
|
+
python -m pip install "git+https://github.com/realspinner/pypsgctrl.git"
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Установка этой версии с TestPyPI в новом виртуальном окружении:
|
|
127
|
+
|
|
128
|
+
```sh
|
|
129
|
+
python -m pip install pyserial
|
|
130
|
+
python -m pip install --index-url https://test.pypi.org/simple/ --no-deps pypsgctrl==0.1.1
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
После клонирования для разработки: `python -m pip install -e .`. Порт: 115200 baud, 8-N-1,
|
|
134
|
+
без flow control. Замените пример macOS на свой порт. Подключение не включает выходы.
|
|
135
|
+
|
|
136
|
+
### Быстрый старт
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
from pypsgctrl import PSG9080, Waveform
|
|
140
|
+
|
|
141
|
+
with PSG9080.connect('/dev/cu.usbserial-2120', timeout=1.0) as generator:
|
|
142
|
+
print(generator.ch1.frequency) # Decimal, Hz
|
|
143
|
+
generator.ch1.waveform = Waveform.SINE
|
|
144
|
+
generator.ch1.frequency = 1000
|
|
145
|
+
generator.ch1.amplitude = '0.100' # Vpp, not RMS
|
|
146
|
+
generator.ch1.offset = 0 # V
|
|
147
|
+
generator.set_outputs(True, False)
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Записи применяются немедленно. Физические значения возвращаются как Decimal;
|
|
151
|
+
строки и Decimal позволяют задать точные дроби. Значения между шагами отвергаются.
|
|
152
|
+
Обмен защищён блокировкой экземпляра. После таймаута соединение следует открыть
|
|
153
|
+
заново. Неописанные в протоколе аппаратные пределы учитывает вызывающий код.
|
|
154
|
+
|
|
155
|
+
### Документация и структура
|
|
156
|
+
|
|
157
|
+
[Справочник на английском](https://github.com/realspinner/pypsgctrl/blob/main/docs/api_reference_en.md) · [Справочник на русском](https://github.com/realspinner/pypsgctrl/blob/main/docs/api_reference_ru.md)
|
|
158
|
+
|
|
159
|
+
| Модуль | Назначение |
|
|
160
|
+
| --- | --- |
|
|
161
|
+
| `pypsgctrl/__init__.py` | Стабильные публичные импорты и версия |
|
|
162
|
+
| `pypsgctrl/device.py` | Соединение, обмен, общие настройки |
|
|
163
|
+
| `pypsgctrl/channel.py` | Свойства канала, частота и управление выходом |
|
|
164
|
+
| `pypsgctrl/registers.py` | Метаданные регистров и преобразование единиц |
|
|
165
|
+
| `pypsgctrl/enums.py` | Перечисления формы, частоты, модуляции и запуска |
|
|
166
|
+
| `pypsgctrl/errors.py` | Исключения драйвера |
|
|
167
|
+
| `pypsgctrl/transport.py` | Контракт внедряемого транспорта |
|
|
168
|
+
|
|
169
|
+
Публичные импорты: `from pypsgctrl import PSG9080, Waveform`.
|
|
170
|
+
Об ошибках можно сообщить через [GitHub Issues](https://github.com/realspinner/pypsgctrl/issues). Комментарии оформлены
|
|
171
|
+
тегами Doxygen. При установленном Doxygen команда `doxygen Doxyfile` из корня репозитория
|
|
172
|
+
генерирует `build/doxygen/html`.
|
|
173
|
+
|
|
174
|
+
### Лицензия
|
|
175
|
+
|
|
176
|
+
MIT License, правообладатель — Oleg Kochetov, 2026. См. [LICENSE](https://github.com/realspinner/pypsgctrl/blob/main/LICENSE).
|
|
177
|
+
PDF производителя и утилиты калибровки рекордера в репозиторий не включены.
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
# API reference: pypsgctrl 0.1.1
|
|
2
|
+
|
|
3
|
+
[Русская версия](api_reference_ru.md)
|
|
4
|
+
|
|
5
|
+
PSG9080 driver for Python >= 3.10. Protocol source: `PSG Communication Protocol.pdf` (manufacturer document, not distributed here).
|
|
6
|
+
Public imports live in `pypsgctrl/__init__.py`; implementation is split into
|
|
7
|
+
`device.py`, `channel.py`, `registers.py`, `enums.py`, `errors.py`, and `transport.py`.
|
|
8
|
+
Serial dependency: `pyserial>=3.5`. Source documentation uses Doxygen comments
|
|
9
|
+
`## @brief`, `@param`, `@return`, and `@exception`.
|
|
10
|
+
|
|
11
|
+
## Quick start
|
|
12
|
+
|
|
13
|
+
From the repository root, install the package and run the tests:
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
python -m pip install .
|
|
17
|
+
python -m unittest discover -s tests -v
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
from pypsgctrl import PSG9080, Waveform
|
|
22
|
+
|
|
23
|
+
with PSG9080.connect('/dev/cu.usbserial-2120', timeout=1.0) as psg:
|
|
24
|
+
print(psg.ch1.frequency) # Decimal, Hz
|
|
25
|
+
print(psg.ch1.amplitude) # Decimal, Vpp
|
|
26
|
+
psg.ch1.waveform = Waveform.SINE
|
|
27
|
+
psg.ch1.frequency = 3000
|
|
28
|
+
psg.ch1.amplitude = '5.000'
|
|
29
|
+
psg.set_outputs(True, False)
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Writes change the device immediately. Opening a port does not enable outputs.
|
|
33
|
+
When synchronization is active, CH1 changes may affect CH2 according to the device settings.
|
|
34
|
+
|
|
35
|
+
## Types, units, and precision
|
|
36
|
+
|
|
37
|
+
Numeric inputs accept int, float, Decimal, IntEnum, and numeric strings.
|
|
38
|
+
Strings and Decimal represent exact fractions. NaN/Infinity are rejected;
|
|
39
|
+
values between register steps are rejected without rounding.
|
|
40
|
+
Physical values and numeric selectors are returned as Decimal; compound registers
|
|
41
|
+
return tuple[Decimal, ...]. Exceptions: enabled returns bool, interface returns
|
|
42
|
+
tuple[int, ...], and read_raw() returns tuple[str, ...]. Reads validate syntax
|
|
43
|
+
and field count but do not enforce the write range on device responses.
|
|
44
|
+
|
|
45
|
+
Frequencies are in Hz, durations in seconds, amplitudes in Vpp, offsets in V,
|
|
46
|
+
angles in degrees, and duty/AM depth in percent. Undocumented hardware limits,
|
|
47
|
+
such as MAXF and maximum amplitude, must be respected by the caller.
|
|
48
|
+
|
|
49
|
+
## PSG9080 connection
|
|
50
|
+
|
|
51
|
+
| API | Purpose |
|
|
52
|
+
| --- | --- |
|
|
53
|
+
| `PSG9080.connect(port, *, timeout=1.0)` | Opens a path/URL using pyserial at 115200, 8-N-1, without flow control. Returns a PSG9080 owning the port. A positive finite timeout applies to reads, writes, and responses. |
|
|
54
|
+
| `PSG9080(transport, *, owns_transport=False, response_timeout=1.0)` | Injects a transport; borrowed by default. |
|
|
55
|
+
| `close()` | Closes the driver and its owned transport; repeated calls are allowed. |
|
|
56
|
+
| `with PSG9080.connect(...) as psg` | Closes the connection even when an exception occurs. |
|
|
57
|
+
| `ch1`, `ch2`, `channel(number)` | Return Channel; number must be 1 or 2, not bool. |
|
|
58
|
+
| `get(name)` | Reads a general register from the catalog below. |
|
|
59
|
+
| `set(name, *values)` | Validates range/resolution and writes all fields. Returns None. |
|
|
60
|
+
| `set_outputs(ch1, ch2)` | Sets both outputs; True/1 enables and False/0 disables. |
|
|
61
|
+
| `set_sync(*, waveform=False, frequency=False, amplitude=False, offset=False, duty=False, external=False)` | Writes six synchronization flags; CH1 is the leader. |
|
|
62
|
+
| `load(slot)` / `save(slot)` / `clear(slot)` | Command 26, operations 111/222/333. slot is a nonnegative integer; the upper limit is undocumented. |
|
|
63
|
+
| `clear_all()` | Clears all memory slots using command 26/444. |
|
|
64
|
+
|
|
65
|
+
Read memory with get(); use the memory methods for writes. Saving, loading,
|
|
66
|
+
and clearing immediately affect memory or settings.
|
|
67
|
+
|
|
68
|
+
## Channel
|
|
69
|
+
|
|
70
|
+
Obtain Channel through the connection. The public constructor
|
|
71
|
+
`Channel(device, number)` does not validate the number; `PSG9080.channel()` does.
|
|
72
|
+
`ch.get(name)` and `ch.set(name, value)` use CHANNEL_REGISTERS.
|
|
73
|
+
frequency/enabled are separate properties, not names in that catalog.
|
|
74
|
+
All properties below support reading and writing.
|
|
75
|
+
|
|
76
|
+
| Property | Codes CH1 / CH2 | Unit | Step | Write range |
|
|
77
|
+
| --- | --- | --- | --- | --- |
|
|
78
|
+
| `waveform` | 11 / 12 | code | 1 | 0..21 or 101..199 |
|
|
79
|
+
| `amplitude` | 15 / 16 | Vpp | 0.001 | 0..device limit |
|
|
80
|
+
| `offset` | 17 / 18 | V | 0.01 | -10..device limit |
|
|
81
|
+
| `duty` | 19 / 20 | % | 0.01 | 0..100 |
|
|
82
|
+
| `phase` | 21 / 22 | ° | 0.01 | 0..359.99 |
|
|
83
|
+
| `modulation_frequency` | 43 / 44 | Hz | 0.001 | 0..1000000 |
|
|
84
|
+
| `am_depth` | 45 / 46 | % | 0.1 | 0..200 |
|
|
85
|
+
| `fm_deviation` | 47 / 48 | Hz | 0.1 | 0..device limit |
|
|
86
|
+
| `fsk_frequency` | 49 / 50 | Hz | 0.1 | 0..device limit |
|
|
87
|
+
| `pm_deviation` | 51 / 52 | ° | 0.1 | 0..359.9 |
|
|
88
|
+
| `pulse_width` | 53 / 54 | s | 1E-9 | 0..0.4 |
|
|
89
|
+
| `pulse_period` | 55 / 56 | s | 1E-8 | 0..4 |
|
|
90
|
+
| `frequency` | 13 / 14 | Hz | Usually 0.001 | >= 0, MAXF depends on the device |
|
|
91
|
+
| `enabled` | 10 (pair) | bool | — | False/True |
|
|
92
|
+
|
|
93
|
+
Assigning enabled reads both output states and preserves the other channel.
|
|
94
|
+
The read/modify/write sequence uses the instance lock. waveform returns a
|
|
95
|
+
Decimal code; use `Waveform(int(ch.waveform))` for known built-in waveforms.
|
|
96
|
+
Arbitrary waveform codes cannot be converted to Waveform.
|
|
97
|
+
|
|
98
|
+
| Method | Purpose |
|
|
99
|
+
| --- | --- |
|
|
100
|
+
| `set_arbitrary_waveform(slot)` | Converts slot 1..99 to waveform code 101..199. |
|
|
101
|
+
| `set_frequency(hz, *, display_unit=FrequencyUnit.HZ)` | Always accepts Hz; selects display units separately. |
|
|
102
|
+
|
|
103
|
+
Assigning ch.frequency selects HZ. Protocol examples use the same 0.001 Hz ticks
|
|
104
|
+
for HZ/KHZ/MHZ. MILLIHZ/UHZ are interpreted as 0.000001/0.000000001 Hz steps;
|
|
105
|
+
these display modes have not been verified on hardware.
|
|
106
|
+
|
|
107
|
+
## General registers
|
|
108
|
+
|
|
109
|
+
Pass compound values as separate arguments: `psg.set('outputs', True, False)`,
|
|
110
|
+
rather than a single tuple. R means read-only; RW means read/write.
|
|
111
|
+
A step of one also applies to selectors and counters.
|
|
112
|
+
|
|
113
|
+
| Name | Code | Fields | Access | Scale: ticks/unit | Write range per field |
|
|
114
|
+
| --- | --- | --- | --- | --- | --- |
|
|
115
|
+
| `outputs` | 10 | 2 | RW | 1 | 0..1 |
|
|
116
|
+
| `interface` | 24 | 4 | RW | 1 | 0..255 (hex selectors) |
|
|
117
|
+
| `sync` | 25 | 6 | RW | 1 | 0..1 |
|
|
118
|
+
| `memory` | 26 | 1 | R | 1 | 0..unspecified |
|
|
119
|
+
| `sound` | 27 | 1 | RW | 1 | 0..1 |
|
|
120
|
+
| `brightness` | 28 | 1 | RW | 1 | 0..100 |
|
|
121
|
+
| `language` | 29 | 1 | RW | 1 | 0..1 |
|
|
122
|
+
| `preset_wave_count` | 30 | 1 | RW | 1 | 0..39 |
|
|
123
|
+
| `arbitrary_wave_count` | 31 | 1 | RW | 1 | 0..99 |
|
|
124
|
+
| `wave_loading` | 32 | 1 | RW | 1 | 0..1 |
|
|
125
|
+
| `frequency_trim` | 33 | 1 | RW | 1 | unspecified..unspecified |
|
|
126
|
+
| `modulation` | 40 | 2 | RW | 1 | 0..7 |
|
|
127
|
+
| `modulation_waveform` | 41 | 2 | RW | 1 | 0..9 |
|
|
128
|
+
| `modulation_source` | 42 | 2 | RW | 1 | 0..1 |
|
|
129
|
+
| `pulse_inversion` | 57 | 2 | RW | 1 | 0..1 |
|
|
130
|
+
| `burst_idle` | 58 | 2 | RW | 1 | 0..2 |
|
|
131
|
+
| `polarity` | 59 | 2 | RW | 1 | 0..1 |
|
|
132
|
+
| `trigger_source` | 60 | 2 | RW | 1 | 0..3 |
|
|
133
|
+
| `burst_count` | 61 | 2 | RW | 1 | 0..1000000000 |
|
|
134
|
+
| `measurement_mode` | 63 | 1 | RW | 1 | 0..1 |
|
|
135
|
+
| `sweep_enabled` | 65 | 2 | RW | 1 | 0..1 |
|
|
136
|
+
| `sweep_start_frequency` | 66 | 1 | RW | 10 | 0..unspecified |
|
|
137
|
+
| `sweep_end_frequency` | 67 | 1 | RW | 10 | 0..unspecified |
|
|
138
|
+
| `sweep_start_amplitude` | 68 | 1 | RW | 1000 | 0..unspecified |
|
|
139
|
+
| `sweep_end_amplitude` | 69 | 1 | RW | 1000 | 0..unspecified |
|
|
140
|
+
| `sweep_start_duty` | 70 | 1 | RW | 100 | 0..100 |
|
|
141
|
+
| `sweep_end_duty` | 71 | 1 | RW | 100 | 0..100 |
|
|
142
|
+
| `voltage_calibration_min` | 72 | 1 | RW | 1 | 0..unspecified |
|
|
143
|
+
| `voltage_calibration_max` | 73 | 1 | RW | 1 | 0..unspecified |
|
|
144
|
+
| `trigger` | 74 | 2 | RW | 1 | 0..1 |
|
|
145
|
+
| `counter` | 80 | 1 | R | 1 | 0..unspecified |
|
|
146
|
+
| `measured_high_frequency` | 81 | 1 | R | 1 | 0..unspecified |
|
|
147
|
+
| `measured_low_frequency` | 82 | 1 | R | 1000 | 0..unspecified |
|
|
148
|
+
| `measured_positive_width` | 83 | 1 | R | 1000000000 | 0..unspecified |
|
|
149
|
+
| `measured_negative_width` | 84 | 1 | R | 1000000000 | 0..unspecified |
|
|
150
|
+
| `measured_period` | 85 | 1 | R | 100000000 | 0..unspecified |
|
|
151
|
+
| `measured_duty` | 86 | 1 | R | 100 | 0..100 |
|
|
152
|
+
|
|
153
|
+
Compound field order and meanings:
|
|
154
|
+
|
|
155
|
+
| Name | Fields / values |
|
|
156
|
+
| --- | --- |
|
|
157
|
+
| outputs | CH1, CH2; 0 off, 1 on |
|
|
158
|
+
| interface | Four hexadecimal UI selectors matching device menus |
|
|
159
|
+
| sync | waveform, frequency, amplitude, offset, duty, external; 0/1 |
|
|
160
|
+
| modulation | CH1, CH2; Modulation enum |
|
|
161
|
+
| modulation_waveform | CH1, CH2; 0 sine, 1 square, 2 triangle, 3 rising sawtooth, 4 falling sawtooth, 5..9 arbitrary 101..105 |
|
|
162
|
+
| modulation_source | CH1, CH2; 0 internal, 1 external |
|
|
163
|
+
| pulse_inversion | CH1, CH2; 0 normal, 1 inverted |
|
|
164
|
+
| burst_idle | CH1, CH2; 0 zero, 1 positive maximum, 2 negative maximum |
|
|
165
|
+
| polarity | CH1, CH2; 0 positive, 1 negative |
|
|
166
|
+
| trigger_source | CH1, CH2; TriggerSource enum |
|
|
167
|
+
| burst_count | Pulse counts for CH1, CH2 |
|
|
168
|
+
| sweep_enabled | Sweep, voltage control; 0/1 |
|
|
169
|
+
| trigger | CH1, CH2; 0/1 |
|
|
170
|
+
|
|
171
|
+
Other selectors: sound 0/1; brightness in percent; language 0 English/1 Chinese;
|
|
172
|
+
wave_loading 0 automatic/1 fast; measurement_mode 0 counter/1 measurement.
|
|
173
|
+
The frequency_trim range and physical unit are undocumented; the API passes
|
|
174
|
+
an integer without scaling. voltage_calibration_min/max are raw integers.
|
|
175
|
+
measured_high_frequency uses a 1 Hz step; measured_low_frequency 0.001 Hz;
|
|
176
|
+
measured_positive_width/negative_width 1 ns; measured_period 10 ns;
|
|
177
|
+
measured_duty 0.01%. sweep_start/end_frequency use 0.1 Hz,
|
|
178
|
+
sweep_start/end_amplitude 0.001 Vpp, and sweep_start/end_duty 0.01%.
|
|
179
|
+
|
|
180
|
+
## Measurement and sweep
|
|
181
|
+
|
|
182
|
+
`configure_measurement(*, dc=False, gate_seconds='0.02', low_frequency=False)`
|
|
183
|
+
writes command 62: AC/DC coupling, gate time 0.001..10 s with a 0.001 s step,
|
|
184
|
+
and high/low frequency mode. Configuration does not start measurement.
|
|
185
|
+
|
|
186
|
+
`configure_sweep(*, channel=1, seconds=10, direction=0, logarithmic=False)`
|
|
187
|
+
writes command 64: channel 1/2, time 0.01..640 s with a 0.01 s step,
|
|
188
|
+
direction 0 increasing/1 decreasing/2 back and forth, linear or logarithmic mode.
|
|
189
|
+
Enable sweep separately. Read commands 62/64 using read_raw(), without unit conversion.
|
|
190
|
+
|
|
191
|
+
```python
|
|
192
|
+
from pypsgctrl import PSG9080
|
|
193
|
+
|
|
194
|
+
with PSG9080.connect('/dev/cu.usbserial-2120') as psg:
|
|
195
|
+
psg.configure_measurement(dc=False, gate_seconds='0.1', low_frequency=True)
|
|
196
|
+
psg.set('measurement_mode', 1)
|
|
197
|
+
print(psg.get('measured_low_frequency')) # Hz
|
|
198
|
+
print(psg.get('measured_duty')) # %
|
|
199
|
+
|
|
200
|
+
psg.set('sweep_start_frequency', 100)
|
|
201
|
+
psg.set('sweep_end_frequency', 10000)
|
|
202
|
+
psg.configure_sweep(channel=1, seconds=10, direction=2, logarithmic=True)
|
|
203
|
+
psg.set('sweep_enabled', 1, 0)
|
|
204
|
+
# Disable sweep when finished:
|
|
205
|
+
psg.set('sweep_enabled', 0, 0)
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
## Enumerations
|
|
209
|
+
|
|
210
|
+
### Waveform
|
|
211
|
+
|
|
212
|
+
| Name | Code |
|
|
213
|
+
| --- | --- |
|
|
214
|
+
| `SINE` | 0 |
|
|
215
|
+
| `SQUARE` | 1 |
|
|
216
|
+
| `PULSE` | 2 |
|
|
217
|
+
| `TRIANGLE` | 3 |
|
|
218
|
+
| `SLOPE` | 4 |
|
|
219
|
+
| `CMOS` | 5 |
|
|
220
|
+
| `DC` | 6 |
|
|
221
|
+
| `PARTIAL_SINE` | 7 |
|
|
222
|
+
| `HALF_WAVE` | 8 |
|
|
223
|
+
| `FULL_WAVE` | 9 |
|
|
224
|
+
| `POSITIVE_LADDER` | 10 |
|
|
225
|
+
| `NEGATIVE_LADDER` | 11 |
|
|
226
|
+
| `POSITIVE_TRAPEZOID` | 12 |
|
|
227
|
+
| `NEGATIVE_TRAPEZOID` | 13 |
|
|
228
|
+
| `NOISE` | 14 |
|
|
229
|
+
| `EXPONENTIAL_RISE` | 15 |
|
|
230
|
+
| `EXPONENTIAL_FALL` | 16 |
|
|
231
|
+
| `LOGARITHMIC_RISE` | 17 |
|
|
232
|
+
| `LOGARITHMIC_FALL` | 18 |
|
|
233
|
+
| `SINKER_PULSE` | 19 |
|
|
234
|
+
| `MULTI_AUDIO` | 20 |
|
|
235
|
+
| `LORENZ` | 21 |
|
|
236
|
+
|
|
237
|
+
### FrequencyUnit
|
|
238
|
+
|
|
239
|
+
| Name | Code |
|
|
240
|
+
| --- | --- |
|
|
241
|
+
| `HZ` | 0 |
|
|
242
|
+
| `KHZ` | 1 |
|
|
243
|
+
| `MHZ` | 2 |
|
|
244
|
+
| `MILLIHZ` | 3 |
|
|
245
|
+
| `MHZ_SMALL` | 3 |
|
|
246
|
+
| `UHZ` | 4 |
|
|
247
|
+
|
|
248
|
+
### Modulation
|
|
249
|
+
|
|
250
|
+
| Name | Code |
|
|
251
|
+
| --- | --- |
|
|
252
|
+
| `AM` | 0 |
|
|
253
|
+
| `FM` | 1 |
|
|
254
|
+
| `PM` | 2 |
|
|
255
|
+
| `ASK` | 3 |
|
|
256
|
+
| `FSK` | 4 |
|
|
257
|
+
| `PSK` | 5 |
|
|
258
|
+
| `PULSE` | 6 |
|
|
259
|
+
| `BURST` | 7 |
|
|
260
|
+
|
|
261
|
+
### TriggerSource
|
|
262
|
+
|
|
263
|
+
| Name | Code |
|
|
264
|
+
| --- | --- |
|
|
265
|
+
| `KEY` | 0 |
|
|
266
|
+
| `INTERNAL` | 1 |
|
|
267
|
+
| `EXTERNAL_AC` | 2 |
|
|
268
|
+
| `EXTERNAL_DC` | 3 |
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
MHZ_SMALL is a compatibility alias for MILLIHZ (millihertz), not megahertz.
|
|
272
|
+
|
|
273
|
+
## Modulation, memory, and arbitrary waveform examples
|
|
274
|
+
|
|
275
|
+
```python
|
|
276
|
+
from pypsgctrl import PSG9080, Modulation, TriggerSource
|
|
277
|
+
|
|
278
|
+
with PSG9080.connect('/dev/cu.usbserial-2120') as psg:
|
|
279
|
+
psg.set('modulation', Modulation.AM, Modulation.BURST)
|
|
280
|
+
psg.set('modulation_source', 0, 0)
|
|
281
|
+
psg.ch1.am_depth = 80
|
|
282
|
+
psg.ch1.modulation_frequency = 500
|
|
283
|
+
psg.set('trigger_source', TriggerSource.KEY, TriggerSource.EXTERNAL_DC)
|
|
284
|
+
psg.set('burst_count', 10, 20)
|
|
285
|
+
psg.ch2.set_arbitrary_waveform(1) # Select existing arbitrary waveform 01
|
|
286
|
+
# Uploading samples is undocumented by this protocol and is not implemented.
|
|
287
|
+
psg.save(52)
|
|
288
|
+
psg.load(52)
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
## Raw API and transport
|
|
292
|
+
|
|
293
|
+
`read_raw(code)` sends `:rCODE=0.\r\n` and returns a tuple of strings.
|
|
294
|
+
`write_raw(code, *fields)` sends `:wCODE=FIELDS.\r\n` and returns None.
|
|
295
|
+
The code must be int in 0..99. Fields are checked against the ASCII alphabet;
|
|
296
|
+
raw-command semantics, field counts, and ranges are not validated.
|
|
297
|
+
Pass integer wire values without a trailing period or CRLF. Physical fractions
|
|
298
|
+
must be scaled beforehand. The read-response code must match the request.
|
|
299
|
+
Write acknowledgments are OK, OK., or the hardware-observed :ok, followed by CRLF.
|
|
300
|
+
|
|
301
|
+
```python
|
|
302
|
+
from pypsgctrl import PSG9080
|
|
303
|
+
|
|
304
|
+
with PSG9080.connect('/dev/cu.usbserial-2120') as psg:
|
|
305
|
+
print(psg.read_raw(13)) # Example: ('000003000000', '0')
|
|
306
|
+
psg.write_raw(24, '0', '1', '0', 'a')
|
|
307
|
+
# Equivalent general API call:
|
|
308
|
+
psg.set('interface', 0, 1, 0, 10)
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
Transport is a typing.Protocol with `write(data: bytes) -> int`,
|
|
312
|
+
`read(size: int = 1) -> bytes`, and `close() -> None`.
|
|
313
|
+
read() must have a finite timeout; the driver cannot interrupt a blocking call.
|
|
314
|
+
response_timeout is checked between read() calls, so a slow transport may exceed
|
|
315
|
+
it by the duration of one read. Partial writes are errors; responses are limited
|
|
316
|
+
to 65536 bytes.
|
|
317
|
+
|
|
318
|
+
```python
|
|
319
|
+
import serial
|
|
320
|
+
from pypsgctrl import PSG9080
|
|
321
|
+
|
|
322
|
+
transport = serial.Serial('/dev/cu.usbserial-2120', 115200,
|
|
323
|
+
timeout=1, write_timeout=1)
|
|
324
|
+
try:
|
|
325
|
+
with PSG9080(transport, owns_transport=False, response_timeout=1) as psg:
|
|
326
|
+
print(psg.ch1.frequency)
|
|
327
|
+
# A borrowed transport remains open.
|
|
328
|
+
finally:
|
|
329
|
+
transport.close()
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
## Register and catalogs
|
|
333
|
+
|
|
334
|
+
`Register(code, scale=Decimal(1), offset=Decimal(0), minimum=Decimal(0),
|
|
335
|
+
maximum=None, count=1, writable=True)` is a frozen dataclass.
|
|
336
|
+
Encoding uses `raw = physical * scale + offset`; decoding performs the inverse.
|
|
337
|
+
minimum/maximum apply to physical values; None means no bound.
|
|
338
|
+
Channel offset has scale=100 and offset=1000: 0 V maps to raw 1000.
|
|
339
|
+
REGISTERS and CHANNEL_REGISTERS are accessible descriptor dictionaries.
|
|
340
|
+
The CH2 code equals the CH1 code + 1. interface, sync, and frequency have special formats.
|
|
341
|
+
|
|
342
|
+
## Errors and concurrency
|
|
343
|
+
|
|
344
|
+
| Exception | Cause |
|
|
345
|
+
| --- | --- |
|
|
346
|
+
| PSGError | Base exception; also access to a closed driver |
|
|
347
|
+
| ProtocolError(PSGError) | Partial write, invalid ASCII/framing/code/field count, or rejected write |
|
|
348
|
+
| PSGTimeoutError(PSGError, TimeoutError) | Incomplete response or expired response deadline |
|
|
349
|
+
| ValueError | Invalid value, resolution, code, channel, field count, or write to a read-only register |
|
|
350
|
+
| KeyError | Unknown get/set name |
|
|
351
|
+
| pyserial exceptions | Port opening or transport errors; propagated without wrapping |
|
|
352
|
+
|
|
353
|
+
After a timeout, close and reopen the connection: the protocol has no transaction
|
|
354
|
+
IDs, so a late response could be mistaken for the next one. Writes are never
|
|
355
|
+
retried automatically. Multiple parameter writes are not an atomic transaction.
|
|
356
|
+
RLock protects exchanges between threads of one instance. A port should belong
|
|
357
|
+
to only one instance/process.
|
|
358
|
+
|
|
359
|
+
## Protocol ambiguities and hardware verification
|
|
360
|
+
|
|
361
|
+
* Frequency units 3/4 follow the interpretation of the examples; not hardware-verified.
|
|
362
|
+
* Width/period wire maxima conflict with the document's physical maxima.
|
|
363
|
+
Conservative limits of 0.4/4 s and steps of 1/10 ns are used.
|
|
364
|
+
* Command 63 is described with two fields in the text but one mode in the table.
|
|
365
|
+
The API uses one mode; use write_raw() for an alternative format.
|
|
366
|
+
* r25 accepts six compact bits or CSV. The repeated n3 in w64 is interpreted as n4.
|
|
367
|
+
* Maximum frequency/amplitude/positive offset/memory slot and the frequency_trim
|
|
368
|
+
range are not fully specified by the document.
|
|
369
|
+
* Initial brightness readback was 101, but writing 101 was clamped to 100 by the
|
|
370
|
+
device. The API write range remains 0..100; readback can exceed it.
|
|
371
|
+
|
|
372
|
+
On 2026-10-07, 13 library tests passed on Python 3.10.6 before publication preparation.
|
|
373
|
+
Hardware checks on `/dev/cu.usbserial-2120`, 115200, 8-N-1 covered reads, brightness
|
|
374
|
+
20/100, CH2 phase, and CH1 waveform/frequency/amplitude: sine 1 kHz/2 Vpp,
|
|
375
|
+
square 2 kHz/4 Vpp, triangle 500 Hz/1 Vpp. The user confirmed visible changes.
|
|
376
|
+
Sine 3 kHz/5 Vpp and brightness 100 were restored. The analog signal was not
|
|
377
|
+
verified using measurement equipment.
|