pyinsts 0.0.3__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.
pyinsts-0.0.3/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) [2024] [zhangxp93]
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.
pyinsts-0.0.3/PKG-INFO ADDED
@@ -0,0 +1,43 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyinsts
3
+ Version: 0.0.3
4
+ Summary: PyVISA仪器控制库,支持Keysight和Rohde&Schwarz仪器
5
+ Author-email: zhangxp93 <937097345@qq.com>
6
+ Requires-Python: >=3.10
7
+ Description-Content-Type: text/markdown
8
+ License-File: LICENSE
9
+ Requires-Dist: colorlog>=6.9.0
10
+ Requires-Dist: pyvisa>=1.14.1
11
+ Requires-Dist: pyyaml>=6.0.3
12
+ Requires-Dist: versioningit>=3.3.0
13
+ Dynamic: license-file
14
+
15
+ [**简体中文**](README.md) | [**English**](README_en.md)
16
+
17
+ <div align="center">
18
+ <h1>
19
+ <strong>PyVISA仪器控制</strong>
20
+ </h1>
21
+ <h5>
22
+ <strong>适用于 Windows、MacOs、Linux</strong><br>
23
+ </h5>
24
+ </div><br>
25
+
26
+ ___
27
+
28
+ - **基于pyvisa库对仪器进行控制,支持USB、GPIB、TCP/IP等连接方式。**
29
+ - **Rohde & Schwarz SCPI**
30
+ - **KeySight SCPI**
31
+
32
+
33
+ ## 必要依赖库安装
34
+ PyVISA 是一个用于控制和通信仪器的 Python 库,使用下面的代码安装:
35
+
36
+ ```
37
+ pip install pyvisa
38
+ ```
39
+
40
+
41
+
42
+ ## 🏷[更新日志](CHANGELOG.md)
43
+
@@ -0,0 +1,29 @@
1
+ [**简体中文**](README.md) | [**English**](README_en.md)
2
+
3
+ <div align="center">
4
+ <h1>
5
+ <strong>PyVISA仪器控制</strong>
6
+ </h1>
7
+ <h5>
8
+ <strong>适用于 Windows、MacOs、Linux</strong><br>
9
+ </h5>
10
+ </div><br>
11
+
12
+ ___
13
+
14
+ - **基于pyvisa库对仪器进行控制,支持USB、GPIB、TCP/IP等连接方式。**
15
+ - **Rohde & Schwarz SCPI**
16
+ - **KeySight SCPI**
17
+
18
+
19
+ ## 必要依赖库安装
20
+ PyVISA 是一个用于控制和通信仪器的 Python 库,使用下面的代码安装:
21
+
22
+ ```
23
+ pip install pyvisa
24
+ ```
25
+
26
+
27
+
28
+ ## 🏷[更新日志](CHANGELOG.md)
29
+
@@ -0,0 +1,21 @@
1
+ [project]
2
+ name = "pyinsts"
3
+ version = "0.0.3"
4
+ description = "PyVISA仪器控制库,支持Keysight和Rohde&Schwarz仪器"
5
+ authors = [
6
+ { name = "zhangxp93", email = "937097345@qq.com" }
7
+ ]
8
+ readme = "README.md"
9
+ requires-python = ">=3.10"
10
+ dependencies = [
11
+ "colorlog>=6.9.0",
12
+ "pyvisa>=1.14.1",
13
+ "pyyaml>=6.0.3",
14
+ "versioningit>=3.3.0",
15
+ ]
16
+
17
+
18
+ [tool.uv.workspace]
19
+ members = [
20
+ "test_app",
21
+ ]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
File without changes
@@ -0,0 +1,15 @@
1
+ def _get_version() -> str:
2
+
3
+ from importlib.resources import files
4
+ from pathlib import Path
5
+
6
+ import versioningit
7
+
8
+ module_path = files("pyinsts")
9
+ if isinstance(module_path, Path):
10
+ return versioningit.get_version(project_dir=Path(module_path).parent.parent)
11
+ else:
12
+ return "0.0"
13
+
14
+
15
+ __version__ = "0.0.3"
File without changes
@@ -0,0 +1,3 @@
1
+ from .log_data import setup_logging
2
+
3
+ __all__ = ["setup_logging"]
@@ -0,0 +1,96 @@
1
+ """
2
+ 日志处理
3
+ """
4
+ import logging
5
+ import os
6
+ from logging.handlers import RotatingFileHandler
7
+ import colorlog # 导入 colorlog
8
+ def setup_logging(log_file=None, log_level=logging.INFO, max_bytes=1 * 1024 * 1024, backup_count=5):
9
+ """
10
+ 配置日志记录,包括日志轮换和控制台输出,支持彩色日志
11
+ :param log_file: 日志文件路径,如果为None则使用默认路径
12
+ :param log_level: 日志等级
13
+ :param max_bytes: 单个日志文件的最大字节数,默认为1MB
14
+ :param backup_count: 保留的日志文件备份数
15
+ """
16
+ if log_file is None:
17
+ log_file = os.getenv('LOG_FILE_PATH', 'docs/log/test.log')
18
+
19
+ # 确保日志文件夹存在
20
+ log_dir = os.path.dirname(log_file)
21
+ if not os.path.exists(log_dir):
22
+ os.makedirs(log_dir)
23
+
24
+ # 创建根记录器
25
+ logger = logging.getLogger()
26
+ logger.setLevel(log_level)
27
+
28
+ # 清除已有的处理器,避免重复添加
29
+ if logger.hasHandlers():
30
+ logger.handlers.clear()
31
+
32
+ # 创建文件处理器,支持日志轮换
33
+ file_handler = RotatingFileHandler(log_file, mode='w', maxBytes=max_bytes, backupCount=backup_count)
34
+ file_handler.setLevel(log_level)
35
+
36
+ # 创建控制台处理器,日志输出到控制台
37
+ console_handler = logging.StreamHandler()
38
+ console_handler.setLevel(log_level)
39
+
40
+ # 定义日志格式
41
+ log_format = '%(asctime)s - %(levelname)-8s - %(message)-60s - [%(filename)-15s:%(lineno)-4d]'
42
+ color_format = "%(log_color)s" + log_format
43
+
44
+ # 设置文件处理器的格式
45
+ file_formatter = logging.Formatter(log_format)
46
+ file_handler.setFormatter(file_formatter)
47
+
48
+ # 设置控制台处理器的彩色格式
49
+ color_formatter = colorlog.ColoredFormatter(
50
+ color_format,
51
+ log_colors={
52
+ 'DEBUG': 'cyan',
53
+ 'INFO': 'green',
54
+ 'WARNING': 'yellow',
55
+ 'ERROR': 'red',
56
+ 'CRITICAL': 'bold_red',
57
+ }
58
+ )
59
+ console_handler.setFormatter(color_formatter)
60
+
61
+ # 将处理器添加到记录器中
62
+ logger.addHandler(file_handler)
63
+ logger.addHandler(console_handler)
64
+
65
+ # 添加日志分隔符
66
+ logger.info("=" * 60)
67
+ logger.info("新日志开始".center(60, ' '))
68
+ logger.info("=" * 60)
69
+
70
+
71
+ def log_data(data):
72
+ """
73
+ 记录数据的日志
74
+ """
75
+ logging.info(f"数据记录: {data}")
76
+
77
+
78
+ # 通用异常处理装饰器
79
+ def log_exceptions(func):
80
+ def wrapper(*args, **kwargs):
81
+ try:
82
+ return func(*args, **kwargs)
83
+ except Exception as e:
84
+ logging.error(f"{func.__name__} 执行时发生错误", exc_info=True)
85
+ raise e
86
+
87
+ return wrapper
88
+
89
+ # 示例:调用日志设置
90
+ if __name__ == "__main__":
91
+ setup_logging(log_file='/Users/zhangxianping/Desktop/pythonProject/SiCore_ChipTest/docs/log//test.log', log_level=logging.DEBUG)
92
+ log_data("这是一个测试数据记录")
93
+ logging.debug("这是一条调试信息")
94
+ logging.warning("这是一条警告信息")
95
+ logging.error("这是一条错误信息")
96
+ logging.critical("这是一条严重错误信息")
@@ -0,0 +1,11 @@
1
+ # libs/__init__.py
2
+ from .baseinstrument import BaseInstrument
3
+ from .rs import Fswp
4
+ from .keysight import *
5
+
6
+ __all__ = \
7
+ [
8
+ 'BaseInstrument',
9
+ 'Fswp',
10
+ 'N9020b',
11
+ ]
@@ -0,0 +1,232 @@
1
+ import logging
2
+ import time
3
+
4
+ import pyvisa
5
+ from typing import Callable, Optional
6
+ import os
7
+ import inspect
8
+ from functools import wraps
9
+
10
+ from pyvisa import VisaIOError
11
+
12
+ from src.pyinsts.libs.data import load_config
13
+
14
+
15
+ def handle_instrument_error(func: Callable) -> Callable:
16
+ """仪器操作错误处理装饰器"""
17
+ @wraps(func)
18
+ def wrapper(*args, **kwargs):
19
+ try:
20
+ return func(*args, **kwargs)
21
+ except Exception as e:
22
+ logging.error(f"{args[0].model} {func.__name__}失败: {e}")
23
+ raise
24
+
25
+ return wrapper
26
+
27
+
28
+ class BaseInstrument:
29
+ """基础仪器控制类"""
30
+
31
+ def __init__(self, address: Optional[str] = None, config_path: str = "config.yaml",
32
+ model: str = "E5052B"):
33
+ """初始化仪器连接
34
+
35
+ Args:
36
+ address: 仪器地址,例如:"USB0::0x0957::0x0E09::MY47402409::0::INSTR"
37
+ config_path: 配置文件路径
38
+ model: 仪器型号
39
+ """
40
+ # 设置基本属性
41
+ self.model = model
42
+ self.address = self._get_instrument_address(address, config_path)
43
+
44
+ # 设置默认超时参数
45
+
46
+ self.opc_timeout = 100 # 默认OPC超时时间100秒
47
+
48
+
49
+ self.opc_poll_interval = 0.01 # 默认OPC轮询间隔0.01秒
50
+
51
+ # 连接仪器
52
+ self.idn = self._connect_instrument()
53
+
54
+ # def __del__(self):
55
+ # """
56
+ # del的调用时机不确定,依赖于Python的垃圾回收机制。
57
+ # :return:
58
+ # """
59
+ # self.close()
60
+
61
+
62
+ def _get_instrument_address(self, address: str, config_path: str) -> str:
63
+ """获取仪器地址
64
+
65
+ 优先使用传入的地址,否则从配置文件读取
66
+ 支持.yaml和.json格式的配置文件
67
+ """
68
+ if address:
69
+ return address
70
+
71
+ # 处理配置文件路径
72
+ if not os.path.isabs(config_path):
73
+ # 获取继承类的文件所在目录
74
+ derived_class_file = inspect.getmodule(self.__class__).__file__
75
+ derived_class_dir = os.path.dirname(
76
+ os.path.abspath(derived_class_file))
77
+ config_path = os.path.join(derived_class_dir, config_path)
78
+
79
+ # 如果没有指定扩展名,先尝试.yaml,再尝试.json
80
+ if not os.path.splitext(config_path)[1]:
81
+ yaml_path = config_path + '.yaml'
82
+ json_path = config_path + '.json'
83
+
84
+ if os.path.exists(yaml_path):
85
+ config_path = yaml_path
86
+ elif os.path.exists(json_path):
87
+ config_path = json_path
88
+
89
+ config = load_config(config_path)
90
+ address = config.get("instruments", {}).get(self.model)
91
+ if not address:
92
+ raise ValueError(f"未找到{self.model}的仪器地址配置")
93
+
94
+ return address
95
+
96
+ @handle_instrument_error
97
+ def _connect_instrument(self) -> None:
98
+ """建立仪器连接,返回仪器标识"""
99
+ max_retries = 3 # 最大重试次数
100
+
101
+ retry_count = 0
102
+
103
+ while retry_count < max_retries:
104
+ try:
105
+ print(f"型号:{self.model}, 地址:{self.address}")
106
+ self.rm = pyvisa.ResourceManager()
107
+ self.instrument = self.rm.open_resource(self.address)
108
+
109
+ self.instrument.write('*CLS')
110
+ logging.info('清除仪器寄存器信息')
111
+
112
+
113
+ # 验证连接
114
+ idn = self.instrument.query("*IDN?")
115
+ logging.info(f'仪器标识:{idn}, 成功连接{self.model}')
116
+ return idn
117
+ except(pyvisa.VisaIOError, pyvisa.VisaTypeError, TimeoutError):
118
+ retry_count += 1
119
+ if retry_count < max_retries:
120
+ logging.info(f"等待1秒后重试...")
121
+ time.sleep(1)
122
+ # 确保之前的连接被关闭
123
+ if hasattr(self, 'instrument'):
124
+ self.instrument.close()
125
+ if hasattr(self, 'rm'):
126
+ self.rm.close()
127
+ else:
128
+ logging.error(f"连接失败次数超过最大重试次数({max_retries})")
129
+ raise
130
+
131
+ return None
132
+
133
+
134
+ @handle_instrument_error
135
+ def write(self, command: str,
136
+ check_complete: bool = False) -> None:
137
+ """写入命令
138
+
139
+ Args:
140
+ command: SCPI命令
141
+ check_complete: 是否等待命令完成
142
+
143
+ example:
144
+ self.write(f'SENSE1:FREQ:CENT 1GHz')
145
+ """
146
+ self.instrument.timeout = 100000
147
+ self.instrument.write(command)
148
+ if check_complete:
149
+ self.wait_opc()
150
+ logging.debug(f"{self.model} 写入: {command}")
151
+
152
+ @handle_instrument_error
153
+ def query(self, command: str,
154
+ check_complete: bool = False) -> str:
155
+ """查询命令
156
+
157
+ Args:
158
+ command: SCPI命令
159
+ check_complete: 是否等待命令完成
160
+
161
+ Returns:
162
+ 查询结果
163
+
164
+ example:
165
+ result = self.query("*IDN?")
166
+ print(result)
167
+ """
168
+ self.instrument.timeout = 20000
169
+ try:
170
+ result = self.instrument.query(command)
171
+ if check_complete:
172
+ self.wait_opc()
173
+ logging.debug(f"{self.model} 查询: {command}, 结果: {result}")
174
+ return result
175
+ except VisaIOError as e:
176
+ logging.error(f"{self.model} 查询失败: {e}")
177
+ raise
178
+
179
+ @handle_instrument_error
180
+ def set_opc_timeout(self, timeout: float=0.1, poll_interval: float = None):
181
+ """临时设置OPC超时时间
182
+
183
+ Args:
184
+ timeout: 新的超时时间(秒)
185
+ poll_interval: 新的轮询间隔(秒),可选
186
+ """
187
+ self.opc_timeout = timeout
188
+ if poll_interval is not None:
189
+ self.opc_poll_interval = poll_interval
190
+ logging.info(f"{self.model} 设置超时时间: {timeout}秒, 轮询间隔: {poll_interval or self.opc_poll_interval}秒")
191
+
192
+ @handle_instrument_error
193
+ def wait_opc(self, timeout=None, opc_poll_interval=None):
194
+ """等待OPC完成
195
+ Args:
196
+ timeout: 超时时间(秒),默认使用self.timeout
197
+ opc_poll_interval: 轮询间隔(秒),默认使用self.poll_interval
198
+ """
199
+ self.opc_timeout = timeout or self.opc_timeout
200
+ try:
201
+ start_time = time.time()
202
+ logging.info(f"开始等待opc?")
203
+
204
+ while True:
205
+ try:
206
+ opc = self.instrument.query("*OPC?")
207
+ if opc.strip() == '1':
208
+ logging.info("opc运行完成")
209
+ break
210
+ except VisaIOError as e:
211
+ logging.warning(f"OPC查询失败: {str(e)}")
212
+ # break # 通信异常时终止
213
+
214
+ elapsed = time.time() - start_time
215
+ if elapsed >= self.opc_timeout:
216
+ logging.warning(f"运行超时(已耗时:{elapsed:.1f}秒)")
217
+ break
218
+ time.sleep(opc_poll_interval or self.opc_poll_interval)
219
+ end_time = time.time()
220
+ total_time = end_time - start_time
221
+ logging.info(f"总运行时间: {total_time:.2f}秒")
222
+ return total_time # 返回时间供外部使用
223
+ except Exception as e:
224
+ logging.error(f"发生错误: {str(e)}")
225
+ return str(e)
226
+
227
+ @handle_instrument_error
228
+ def close(self) -> None:
229
+ """关闭仪器连接"""
230
+ self.instrument.close()
231
+ logging.info(f"{self.model} 连接已关闭")
232
+
@@ -0,0 +1,3 @@
1
+ from .load_config import load_config
2
+
3
+ __all__ = ['load_config']
@@ -0,0 +1,47 @@
1
+ import os
2
+ import json
3
+ import yaml
4
+ import logging
5
+
6
+
7
+ def load_config(config_path="config.yaml"):
8
+ """加载配置文件,支持YAML和JSON格式
9
+
10
+ Args:
11
+ config_path: 配置文件路径(.yaml或.json)
12
+
13
+ Returns:
14
+ 配置字典
15
+ """
16
+ if not os.path.exists(config_path):
17
+ logging.error(f"配置文件未找到: {config_path}")
18
+ raise FileNotFoundError(f"配置文件未找到: {config_path}")
19
+
20
+ file_ext = os.path.splitext(config_path)[1].lower()
21
+
22
+ with open(config_path, "r", encoding="utf-8") as file:
23
+ try:
24
+ if file_ext in ['.yaml', '.yml']:
25
+ return yaml.safe_load(file)
26
+ elif file_ext == '.json':
27
+ return json.load(file)
28
+ else:
29
+ error_msg = f"不支持的配置文件格式: {file_ext}"
30
+ logging.error(error_msg)
31
+ raise ValueError(error_msg)
32
+ except (yaml.YAMLError, json.JSONDecodeError) as e:
33
+ error_msg = f"配置文件格式错误: {e}"
34
+ logging.error(error_msg)
35
+ raise ValueError(error_msg)
36
+
37
+ def get_config_path(config_path):
38
+ """获取当前工作目录"""
39
+ # 获取当前工作目录
40
+ current_dir = os.getcwd()
41
+ # 拼接配置文件的路径
42
+ config_path = os.path.join(current_dir, config_path)
43
+ return config_path
44
+
45
+
46
+ if __name__ == '__main__':
47
+ config = get_config_path("config.yaml")