|
马上注册,结交更多好友,享用更多功能^_^
您需要 登录 才可以下载或查看,没有账号?立即注册
x
import sys
import re
import time
import serial
import logging
from functools import partial
from PyQt5.QtWidgets import (
QApplication, QWidget, QCheckBox, QPushButton, QLabel,
QGridLayout, QLineEdit, QTextEdit, QVBoxLayout, QGroupBox,
QHBoxLayout, QComboBox, QFormLayout, QSpacerItem, QSizePolicy
)
from PyQt5.QtGui import QFont
from PyQt5.QtCore import QTimer, pyqtSignal, Qt, QThread, QMetaObject, Qt
from datetime import datetime
import serial.tools.list_ports
import unittest
# 配置日志系统
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s [%(levelname)s] %(message)s',
handlers=[
logging.StreamHandler(sys.stdout),
logging.FileHandler("auto_test_app.log", encoding='utf-8')
]
)
logger = logging.getLogger(__name__)
class PowerMeterThread(QThread):
data_ready = pyqtSignal(dict)
def __init__(self, parent=None):
super().__init__(parent)
self.parent = parent
self.running = True
def run(self):
while self.running:
if not self.parent.port_states['POWER_METER']['open']:
self.msleep(1000)
continue
commands = {
'Urms': b':NUMeric:NORMal:VALue? 1\r\n',
'Irms': b':NUMeric:NORMal:VALue? 6\r\n',
'P': b':NUMeric:NORMal:VALue? 11\r\n'
}
result = {}
try:
for key, cmd in commands.items():
self.parent._clear_serial_buffer('POWER_METER')
self.parent.serial_ports['POWER_METER'].write(cmd)
time.sleep(0.1)
response = self.parent.serial_ports['POWER_METER'].readline()
self.parent._process_power_meter_response(key, response)
result[key] = self.parent.power_meter_data[key]
self.data_ready.emit(result)
except serial.SerialException as e:
logger.error(f"Power meter error: {str(e)}")
self.msleep(self.parent.POWER_METER_QUERY_INTERVAL)
def stop(self):
self.running = False
self.wait()
class PowerTesterLogic:
def _format_measurement(self, value, unit):
return self.logic.format_measurement(value, unit)
def _send_voltage_command(self, value, unit):
if unit == 'mV':
value = value / 1000.0
# 示例发送命令到 LMCI
# self.serial_ports['LMCI'].write(f"VOLT {value}\n".encode())
# 示例发送命令到 LMCI
print(f"Sent voltage command: V = {value} V")
class AutoTestApp(QWidget):
serial_port_opened = pyqtSignal(str, bool)
POWER_METER_QUERY_INTERVAL = 1000 # 毫秒
SERIAL_REFRESH_INTERVAL = 2000 # 毫秒
def __init__(self):
super().__init__()
self.logic = PowerTesterLogic()
self._init_serial_ports()
self._init_ui_state()
self._setup_serial_parameters()
self.initUI()
self._setup_timers()
self._connect_signals()
# 日志控制变量
self.last_power_error_time = 0
self.power_error_count = 0
# 初始化线程
self.power_meter_thread = PowerMeterThread(self)
self.power_meter_thread.data_ready.connect(self._update_power_meter_display_from_thread)
# 启动线程
self.power_meter_thread.start()
def _init_serial_ports(self):
"""初始化串口相关配置"""
self.serial_ports = {
'LMCI': serial.Serial(baudrate=115200, timeout=1),
'LOAD': serial.Serial(baudrate=9600, timeout=1),
'POWER_METER': serial.Serial(baudrate=9600, timeout=1)
}
self.port_states = {
typ: {'open': False, 'port': None}
for typ in ['LMCI', 'LOAD', 'POWER_METER']
}
def _init_ui_state(self):
"""初始化UI状态"""
self.load_active = False
self.power_meter_data = {
'Urms': None,
'Irms': None,
'P': None
}
self.custom_voltage = {'value': None, 'unit': 'V'}
self.custom_load = {'value': None}
def _setup_serial_parameters(self):
"""配置串口参数"""
for port in self.serial_ports.values():
port.bytesize = serial.EIGHTBITS
port.parity = serial.PARITY_NONE
port.stopbits = serial.STOPBITS_ONE
port.rts = False
def initUI(self):
"""初始化用户界面"""
self.setWindowTitle('Auto Test System - Modern UI')
screen = QApplication.primaryScreen().availableGeometry()
width = int(screen.width() * 0.8)
height = int(screen.height() * 0.7)
self.resize(width, height)
self.setStyleSheet("background-color: #f4f4f4;")
main_layout = QVBoxLayout()
main_layout.setSpacing(20)
main_layout.setContentsMargins(30, 20, 30, 20)
main_layout.addWidget(self._create_voltage_group())
main_layout.addWidget(self._create_load_group())
main_layout.addLayout(self._create_feedback_section())
main_layout.addLayout(self._create_control_section())
main_layout.addWidget(self._create_status_bar())
self.setLayout(main_layout)
def _create_voltage_group(self):
group = QGroupBox("Voltage Settings")
layout = QFormLayout()
voltage_row = QHBoxLayout()
self.standard_voltage_buttons = [QCheckBox(f"{v} V") for v in [120, 220, 277, 347]]
for btn in self.standard_voltage_buttons:
voltage_row.addWidget(btn)
voltage_row.addStretch()
custom_voltage_row = QHBoxLayout()
self.custom_voltage_input = QLineEdit()
self.voltage_unit_combo = QComboBox()
self.voltage_unit_combo.addItems(["mV", "V"])
send_voltage_btn = QPushButton("Send Voltage")
send_voltage_btn.setStyleSheet(self.get_button_style("#2196F3"))
custom_voltage_row.addWidget(self.custom_voltage_input)
custom_voltage_row.addWidget(self.voltage_unit_combo)
custom_voltage_row.addWidget(send_voltage_btn)
layout.addRow("Standard Voltage:", voltage_row)
layout.addRow("Custom Voltage:", custom_voltage_row)
group.setLayout(layout)
return group
def _create_load_group(self):
group = QGroupBox("Load Settings")
layout = QFormLayout()
load_row = QHBoxLayout()
self.standard_load_buttons = [QCheckBox(f"{i} A") for i in range(1, 9)]
for btn in self.standard_load_buttons:
load_row.addWidget(btn)
load_row.addStretch()
select_all_btn = QPushButton("Select All")
clear_all_btn = QPushButton("Clear All")
select_all_btn.setStyleSheet(self.get_button_style("#4CAF50"))
clear_all_btn.setStyleSheet(self.get_button_style("#f44336"))
btn_row = QHBoxLayout()
btn_row.addWidget(select_all_btn)
btn_row.addWidget(clear_all_btn)
custom_load_row = QHBoxLayout()
self.custom_load_input = QLineEdit()
send_load_btn = QPushButton("Send Load")
send_load_btn.setStyleSheet(self.get_button_style("#FF9800"))
custom_load_row.addWidget(self.custom_load_input)
custom_load_row.addWidget(send_load_btn)
layout.addRow("Standard Load:", load_row)
layout.addRow("", btn_row)
layout.addRow("Custom Load:", custom_load_row)
group.setLayout(layout)
select_all_btn.clicked.connect(self._select_all_loads)
clear_all_btn.clicked.connect(self._clear_all_loads)
return group
def get_button_style(self, color):
return f"""
QPushButton {{
background-color: {color};
color: white;
padding: 6px 12px;
border: none;
border-radius: 4px;
font-weight: bold;
}}
QPushButton:hover {{
background-color: {self.darken_color(color)};
}}
QPushButton:pressed {{
background-color: {self.lighten_color(color)};
}}
"""
def darken_color(self, hex_color):
r, g, b = int(hex_color[1:3], 16), int(hex_color[3:5], 16), int(hex_color[5:7], 16)
r, g, b = max(r - 30, 0), max(g - 30, 0), max(b - 30, 0)
return f"#{r:02X}{g:02X}{b:02X}"
def lighten_color(self, hex_color):
r, g, b = int(hex_color[1:3], 16), int(hex_color[3:5], 16), int(hex_color[5:7], 16)
r, g, b = min(r + 30, 255), min(g + 30, 255), min(b + 30, 255)
return f"#{r:02X}{g:02X}{b:02X}"
def _create_feedback_section(self):
layout = QHBoxLayout()
self.system_msg_box = QTextEdit()
self.system_msg_box.setReadOnly(True)
self.system_msg_box.setPlaceholderText("System messages...")
self.power_meter_box = QTextEdit()
self.power_meter_box.setReadOnly(True)
self.power_meter_box.setPlaceholderText("Power meter data...")
layout.addWidget(self.system_msg_box, stretch=1)
layout.addWidget(self.power_meter_box, stretch=1)
return layout
def _create_control_section(self):
layout = QHBoxLayout()
layout.setSpacing(10)
serial_ctrl_layout = self._create_serial_controls()
layout.addLayout(serial_ctrl_layout)
load_ctrl_layout = self._create_load_control_buttons()
layout.addLayout(load_ctrl_layout)
layout.addStretch()
return layout
def _create_serial_controls(self):
layout = QHBoxLayout()
self.serial_combos = {typ: QComboBox() for typ in ['LMCI', 'LOAD', 'POWER_METER']}
self._populate_serial_combos()
for typ in ['LMCI', 'LOAD', 'POWER_METER']:
layout.addWidget(QLabel(f"{typ}:"))
layout.addWidget(self.serial_combos[typ])
return layout
def _create_load_control_buttons(self):
layout = QHBoxLayout()
self.load_on_btn = QPushButton("Load ON")
self.load_off_btn = QPushButton("Load OFF")
self.load_on_btn.setStyleSheet(self.get_button_style("#4CAF50"))
self.load_off_btn.setStyleSheet(self.get_button_style("#f44336"))
layout.addWidget(self.load_on_btn)
layout.addWidget(self.load_off_btn)
return layout
def _create_status_bar(self):
self.status_label = QLabel()
self.update_datetime_label()
return self.status_label
def _populate_serial_combos(self):
ports = [port.device for port in serial.tools.list_ports.comports()]
for typ, combo in self.serial_combos.items():
combo.clear()
combo.addItems(ports)
if self.port_states[typ]['open']:
combo.setCurrentText(self.port_states[typ]['port'])
def _setup_timers(self):
self.datetime_timer = QTimer(self)
self.datetime_timer.timeout.connect(self.update_datetime_label)
self.datetime_timer.start(1000)
self.serial_refresh_timer = QTimer(self)
self.serial_refresh_timer.timeout.connect(self.refresh_serial_ports)
self.serial_refresh_timer.start(self.SERIAL_REFRESH_INTERVAL)
def _connect_signals(self):
for btn in self.standard_voltage_buttons:
btn.toggled.connect(self._on_voltage_selection_changed)
self.custom_voltage_input.textChanged.connect(self._on_custom_voltage_changed)
for btn in self.standard_load_buttons:
btn.toggled.connect(self._on_load_selection_changed)
self.custom_load_input.textChanged.connect(self._on_custom_load_changed)
for typ, combo in self.serial_combos.items():
combo.currentTextChanged.connect(partial(self._handle_serial_selection, port_type=typ))
self.load_on_btn.clicked.connect(self._activate_load)
self.load_off_btn.clicked.connect(self._deactivate_load)
def _handle_serial_selection(self, port_name, port_type):
current_port = self.port_states[port_type]['port']
if port_name and port_name != current_port:
self._close_serial_port(port_type)
self._open_serial_port(port_name, port_type)
def _open_serial_port(self, port_name, port_type):
try:
port = self.serial_ports[port_type]
port.port = port_name
port.open()
time.sleep(0.5)
if port.is_open:
self.port_states[port_type].update({'open': True, 'port': port_name})
logger.info(f"{port_type} connected: {port_name}")
self.serial_port_opened.emit(port_type, True)
else:
logger.warning(f"Failed to open {port_type} port")
except Exception as e:
logger.error(f"{port_type} connection error: {str(e)}")
def _close_serial_port(self, port_type):
if self.port_states[port_type]['open']:
try:
self.serial_ports[port_type].close()
self.port_states[port_type].update({'open': False, 'port': None})
logger.info(f"{port_type} port closed")
except Exception as e:
logger.error(f"Error closing {port_type} port: {str(e)}")
def refresh_serial_ports(self):
ports = [port.device for port in serial.tools.list_ports.comports()]
for typ, combo in self.serial_combos.items():
current = combo.currentText()
combo.blockSignals(True)
combo.clear()
combo.addItems(ports)
if self.port_states[typ]['open']:
combo.setCurrentText(self.port_states[typ]['port'])
else:
combo.setCurrentIndex(-1)
combo.blockSignals(False)
def _update_power_meter_display_from_thread(self, data):
self.power_meter_data.update(data)
self._update_power_meter_display()
def _process_power_meter_response(self, key, response):
try:
decoded = response.decode().strip()
value = float(re.search(r"[-+]?\d*\.?\d+(?:[eE][-+]?\d+)?", decoded).group())
self.power_meter_data[key] = value
except (UnicodeDecodeError, AttributeError, ValueError) as e:
logger.warning(f"Invalid {key} data: {str(e)}")
self.power_meter_data[key] = None
def _clear_serial_buffer(self, port_type):
port = self.serial_ports[port_type]
while port.in_waiting > 0:
port.read(port.in_waiting)
def _update_power_meter_display(self):
text = (
"Power Meter:\n"
f"Urms: {self._format_measurement(self.power_meter_data['Urms'], 'V')}\n"
f"Irms: {self._format_measurement(self.power_meter_data['Irms'], 'A')}\n"
f"Power: {self._format_measurement(self.power_meter_data['P'], 'W')}"
)
self.power_meter_box.setText(text)
def update_datetime_label(self):
self.status_label.setText(f"Last update: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
def _on_voltage_selection_changed(self, checked):
sender = self.sender()
if checked:
for btn in self.standard_voltage_buttons:
if btn != sender:
btn.setChecked(False)
self.custom_voltage_input.clear()
self.custom_voltage_input.setEnabled(not any(
btn.isChecked() for btn in self.standard_voltage_buttons))
def _on_custom_voltage_changed(self, text):
has_input = bool(text.strip())
for btn in self.standard_voltage_buttons:
btn.setEnabled(not has_input)
def _on_load_selection_changed(self, checked):
self.custom_load_input.setEnabled(not any(
btn.isChecked() for btn in self.standard_load_buttons))
def _on_custom_load_changed(self, text):
has_input = bool(text.strip())
for btn in self.standard_load_buttons:
btn.setEnabled(not has_input)
def _select_all_loads(self):
for btn in self.standard_load_buttons:
btn.setChecked(True)
self.custom_load_input.clear()
def _clear_all_loads(self):
for btn in self.standard_load_buttons:
btn.setChecked(False)
self.custom_load_input.clear()
def _send_voltage(self):
selected = [btn.text() for btn in self.standard_voltage_buttons if btn.isChecked()]
custom = self.custom_voltage_input.text().strip()
if custom:
try:
value = float(custom)
unit = self.voltage_unit_combo.currentText()
self._send_voltage_command(value, unit)
logger.info(f"Voltage set: {value} {unit}")
except ValueError:
logger.warning("Invalid voltage value!")
elif selected:
voltage = selected[0].split()[0]
self._send_voltage_command(float(voltage), 'V')
logger.info(f"Voltage set: {selected[0]}")
else:
logger.warning("No voltage selected!")
def _send_load(self):
selected = [btn.text() for btn in self.standard_load_buttons if btn.isChecked()]
custom = self.custom_load_input.text().strip()
if custom:
try:
value = float(custom)
self._send_load_command(value)
logger.info(f"Load set: {value} A")
except ValueError:
logger.warning("Invalid load value!")
elif selected:
loads = [btn.text().split()[0] for btn in self.standard_load_buttons if btn.isChecked()]
logger.info(f"Load set: {', '.join(loads)} A")
else:
logger.warning("No load selected!")
def _send_load_command(self, value):
if not self.port_states['LOAD']['open']:
logger.warning("LOAD port not connected!")
return
try:
command = f"CURR {value}\n".encode()
self.serial_ports['LOAD'].write(command)
logger.info(f"Sent load command: I = {value} A")
self.serial_ports['LOAD'].write(b"CURR?\n")
response = self.serial_ports['LOAD'].readline().decode().strip()
if abs(float(response) - value) < 0.01:
logger.info("Load set successfully!")
self.load_active = True
self._update_load_buttons()
else:
logger.warning("Load setting verification failed!")
except serial.SerialException as e:
logger.error(f"Load communication error: {str(e)}")
except ValueError:
logger.warning("Invalid load response format!")
def _activate_load(self):
if self.port_states['LOAD']['open']:
try:
self.serial_ports['LOAD'].write(b"LOAD:ON\n")
self.load_active = True
self._update_load_buttons()
logger.info("Load activated")
except serial.SerialException as e:
logger.error(f"Load activation failed: {str(e)}")
else:
logger.warning("LOAD port not connected!")
def _deactivate_load(self):
if self.port_states['LOAD']['open']:
try:
self.serial_ports['LOAD'].write(b"LOAD:OFF\n")
self.load_active = False
self._update_load_buttons()
logger.info("Load deactivated")
except serial.SerialException as e:
logger.error(f"Load deactivation failed: {str(e)}")
else:
logger.warning("LOAD port not connected!")
def _update_load_buttons(self):
self.load_on_btn.setEnabled(not self.load_active)
self.load_off_btn.setEnabled(self.load_active)
def closeEvent(self, event):
self.datetime_timer.stop()
self.serial_refresh_timer.stop()
self.power_meter_thread.stop()
for port_type in ['LMCI', 'LOAD', 'POWER_METER']:
self._close_serial_port(port_type)
event.accept()
# ====================
# 单元测试部分
# ====================
class TestAutoTestApp(unittest.TestCase):
def setUp(self):
self.logic = PowerTesterLogic()
def test_format_measurement(self):
self.assertEqual(app._format_measurement(220.345, 'V'), "220.345 V")
self.assertEqual(app._format_measurement(None, 'A'), "N/A")
class TestPowerTesterLogic(unittest.TestCase):
def setUp(self):
self.logic = PowerTesterLogic()
def test_format_measurement(self):
self.assertEqual(self.logic.format_measurement(220.345, 'V'), "220.345 V")
self.assertEqual(self.logic.format_measurement(None, 'A'), "N/A")
if __name__ == '__main__':
import sys
unittest.main(exit=False)
app = QApplication(sys.argv)
ex = AutoTestApp()
ex.show()
sys.exit(app.exec_())
|
|