LaptopTester/tester.py
bearincapsule e17264fb95 refactor for public release
split into config/scanner/sheet_manager modules; hardware lists and
column map moved to sheet_config.json (example provided); fix crash
on submit (missing inp_tester), row=2 overwrite bug, GPU token
matching, SSD decimal-vs-binary mismatch, net accounts returncode
check, rescan page switch; tools/ gitignored with download README
2026-06-11 19:53:17 +03:00

299 lines
11 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Laptop QA wizard: scans hardware, collects test results, pushes them to Google Sheets.
import ctypes
import os
import subprocess
import sys
from PyQt5.QtCore import QSettings, Qt
from PyQt5.QtGui import QFont, QIcon
from PyQt5.QtWidgets import (
QApplication, QComboBox, QFormLayout, QGridLayout, QGroupBox, QHBoxLayout,
QLabel, QLineEdit, QMainWindow, QMessageBox, QPushButton, QScrollArea,
QStackedWidget, QTextEdit, QVBoxLayout, QWidget,
)
from config import get_base_path, load_config
from scanner import HardwareScanner
from sheet_manager import SheetManager
# key (config column), label, options, optional tool button: text + command
CHECKS = [
("Sound", "Звук", ["+ / +", "+ / -", "- / +", "- / -"], "▶️", "sound_test.mp3"),
("Screen", "Экран", ["+", "-", "Нет инфы"], "🖥️", "IsMyLcdOK.exe"),
("Touchscreen", "Тачскрин", ["-", "+", "Его нет"], "👆", "IsMyTouchScreenOK_x64.exe"),
("Kbd", "Клавиатура", ["+", "-", "Нет инфы"], "⌨️", "KeyboardTest.exe"),
("Camera", "Камера", ["+", "-", "Нет инфы"], "📷", "Camera.exe"),
("Mic", "Микрофон", ["+", "-", "Нет инфы"], "🎤", "control mmsys.cpl sounds"),
("Touchpad_Sens", "Тачпад (Сенсор)", ["+", "-", "Нет инфы"], None, None),
("Touchpad_Btn", "Тачпад (Кнопки)", ["+", "-", "Их нет"], None, None),
("Ports", "Разъемы", ["+", "-", "Нет инфы"], None, None),
("Biometrics", "Сканер лица/пальца", ["- / -", "+ / +", "+ / -", "- / +"], "🔐", "devmgmt.msc"),
("CMOS", "Батарейка CMOS", ["+", "-", "Нет инфы"], None, None),
("Bios_Pass", "Пароль BIOS", ["Сброшен", "Не сброшен"], None, None),
("Win_Pass", "Срок пароля Win", ["Сброшен", "Не сброшен"], None, None),
("LTE", "LTE", ["LTE", "-"], None, None),
("Bat_Conn", "Подключение АКБ", ["Подключен", "Не подключен"], None, None),
("Drivers", "Драйверы", ["+", "-", "Нет инфы"], None, None),
("Colors", "Цвета", ["+", "-", "Нет инфы"], None, None),
]
def run_tool(filename: str):
path = os.path.join(get_base_path(), "tools", filename)
if os.path.exists(path):
try:
os.startfile(path)
except OSError as e:
QMessageBox.critical(None, "Ошибка", str(e))
elif "Camera" in filename:
subprocess.Popen("start microsoft.windows.camera:", shell=True)
else:
QMessageBox.critical(None, "Ошибка", f"Файл не найден:\n{path}")
def is_admin() -> bool:
try:
return bool(ctypes.windll.shell32.IsUserAnAdmin())
except OSError:
return False
class TesterApp(QMainWindow):
def __init__(self, cfg):
super().__init__()
self.cfg = cfg
self.setWindowTitle("Laptop QA Wizard")
self.resize(600, 800)
base = get_base_path()
self.settings = QSettings(os.path.join(base, "settings.ini"), QSettings.IniFormat)
icon_path = os.path.join(base, "icon.ico")
if os.path.exists(icon_path):
self.setWindowIcon(QIcon(icon_path))
if not is_admin():
QMessageBox.warning(self, "Внимание", "Запустите от Админа!")
self.scanner = None
self.init_ui()
self.start_scan()
def init_ui(self):
self.stack = QStackedWidget()
self.setCentralWidget(self.stack)
self.page_load = QWidget()
l_load = QVBoxLayout(self.page_load)
self.lbl_load = QLabel("Сканирование...")
self.lbl_load.setAlignment(Qt.AlignCenter)
self.lbl_load.setFont(QFont("Arial", 16))
l_load.addWidget(self.lbl_load)
self.stack.addWidget(self.page_load)
self.page_hw = QWidget()
self.init_hw_ui()
self.stack.addWidget(self.page_hw)
self.page_tests = QWidget()
self.init_tests_ui()
self.stack.addWidget(self.page_tests)
def make_combo(self, items) -> QComboBox:
cb = QComboBox()
cb.addItems(items)
return cb
def init_hw_ui(self):
layout = QVBoxLayout(self.page_hw)
scroll = QScrollArea()
content = QWidget()
form = QFormLayout(content)
self.inp_serial = QLineEdit()
self.inp_brand = self.make_combo(self.cfg.brands)
self.inp_model = self.make_combo(self.cfg.models)
self.inp_cpu = self.make_combo(self.cfg.cpus)
self.inp_ram = self.make_combo(self.cfg.ram)
self.inp_ssd_vol = self.make_combo(self.cfg.ssd)
self.inp_ssd_info = QLineEdit()
self.inp_gpu = self.make_combo(self.cfg.gpus)
hb_bat = QHBoxLayout()
self.inp_bat1_cyc = QLineEdit()
self.inp_bat1_cyc.setPlaceholderText("Циклы")
self.inp_bat1_cap = QLineEdit()
self.inp_bat1_cap.setPlaceholderText("Емкость")
btn_bat = QPushButton("🔋 BatView")
btn_bat.clicked.connect(lambda: run_tool("battery_view.exe"))
hb_bat.addWidget(self.inp_bat1_cyc)
hb_bat.addWidget(self.inp_bat1_cap)
hb_bat.addWidget(btn_bat)
self.grp_bat2 = QGroupBox("Вторая батарея")
self.grp_bat2.setCheckable(True)
self.grp_bat2.setChecked(False)
hb_bat2 = QHBoxLayout(self.grp_bat2)
self.inp_bat2_cyc = QLineEdit()
self.inp_bat2_cyc.setPlaceholderText("Циклы 2")
self.inp_bat2_cap = QLineEdit()
self.inp_bat2_cap.setPlaceholderText("Емкость 2")
hb_bat2.addWidget(self.inp_bat2_cyc)
hb_bat2.addWidget(self.inp_bat2_cap)
form.addRow("Серийный номер:", self.inp_serial)
form.addRow("Бренд:", self.inp_brand)
form.addRow("Модель:", self.inp_model)
form.addRow("CPU:", self.inp_cpu)
form.addRow("RAM:", self.inp_ram)
form.addRow("SSD Объем:", self.inp_ssd_vol)
form.addRow("SSD Инфо:", self.inp_ssd_info)
form.addRow("GPU:", self.inp_gpu)
form.addRow("АКБ 1:", hb_bat)
form.addRow(self.grp_bat2)
self.inp_url = QLineEdit()
self.inp_url.setPlaceholderText("URL Таблицы")
self.inp_url.setText(self.settings.value("sheet_url", ""))
form.addRow("Google URL:", self.inp_url)
scroll.setWidget(content)
scroll.setWidgetResizable(True)
layout.addWidget(scroll)
h_btns = QHBoxLayout()
btn_rescan = QPushButton("🔄 Рескан")
btn_rescan.clicked.connect(self.start_scan)
btn_next = QPushButton("Далее ->")
btn_next.setFixedHeight(40)
btn_next.clicked.connect(lambda: self.stack.setCurrentIndex(2))
h_btns.addWidget(btn_rescan)
h_btns.addWidget(btn_next)
layout.addLayout(h_btns)
def init_tests_ui(self):
layout = QVBoxLayout(self.page_tests)
scroll = QScrollArea()
content = QWidget()
grid = QGridLayout(content)
self.checks = {}
for r, (key, label, opts, bt_txt, bt_cmd) in enumerate(CHECKS):
cb = self.make_combo(opts)
grid.addWidget(QLabel(label), r, 0)
grid.addWidget(cb, r, 1)
if bt_txt:
b = QPushButton(bt_txt)
if bt_cmd.endswith((".exe", ".mp3")):
b.clicked.connect(lambda _, c=bt_cmd: run_tool(c))
else:
b.clicked.connect(lambda _, c=bt_cmd: subprocess.Popen(c, shell=True))
grid.addWidget(b, r, 2)
self.checks[key] = cb
row = len(CHECKS)
grid.addWidget(QLabel("Комментарий:"), row, 0)
self.inp_comment = QTextEdit()
self.inp_comment.setMaximumHeight(60)
grid.addWidget(self.inp_comment, row, 1, 1, 2)
scroll.setWidget(content)
scroll.setWidgetResizable(True)
layout.addWidget(scroll)
h_btns = QHBoxLayout()
btn_back = QPushButton("<- Назад")
btn_back.clicked.connect(lambda: self.stack.setCurrentIndex(1))
btn_fin = QPushButton("🚀 ОТПРАВИТЬ")
btn_fin.setStyleSheet("background-color: #4CAF50; color: white; font-weight: bold;")
btn_fin.setFixedHeight(40)
btn_fin.clicked.connect(self.finish_testing)
h_btns.addWidget(btn_back)
h_btns.addWidget(btn_fin)
layout.addLayout(h_btns)
def start_scan(self):
if self.scanner and self.scanner.isRunning():
return
self.stack.setCurrentIndex(0)
self.scanner = HardwareScanner(self.cfg)
self.scanner.log.connect(self.lbl_load.setText)
self.scanner.finished.connect(self.on_scan_finished)
self.scanner.start()
def on_scan_finished(self, data):
self.inp_serial.setText(data.get("Serial", ""))
self.inp_ssd_info.setText(data.get("SSD_Info", ""))
def set_combo(cb, val):
if not val:
return
idx = cb.findText(val)
if idx >= 0:
cb.setCurrentIndex(idx)
set_combo(self.inp_brand, data.get("Brand", ""))
set_combo(self.inp_model, data.get("Model", ""))
set_combo(self.inp_cpu, data.get("CPU", ""))
set_combo(self.inp_ram, data.get("RAM", ""))
set_combo(self.inp_ssd_vol, data.get("SSD_Vol", ""))
set_combo(self.inp_gpu, data.get("GPU", ""))
if data.get("Win_Pass") == "Сброшен":
self.checks["Win_Pass"].setCurrentText("Сброшен")
self.stack.setCurrentIndex(1)
def finish_testing(self):
serial = self.inp_serial.text().strip()
if not serial:
QMessageBox.warning(self, "Ошибка", "Пустой серийный номер!")
return
url = self.inp_url.text().strip()
if not url:
QMessageBox.warning(self, "Ошибка", "Укажите URL таблицы!")
return
data = {
"Serial": serial,
"Brand": self.inp_brand.currentText(),
"Model": self.inp_model.currentText(),
"CPU": self.inp_cpu.currentText(),
"RAM": self.inp_ram.currentText(),
"SSD_Vol": self.inp_ssd_vol.currentText(),
"SSD_Info": self.inp_ssd_info.text(),
"GPU": self.inp_gpu.currentText(),
"Bat1_Cyc": self.inp_bat1_cyc.text(),
"Bat1_Cap": self.inp_bat1_cap.text(),
"Comment": self.inp_comment.toPlainText(),
}
for key, cb in self.checks.items():
data[key] = cb.currentText()
if self.grp_bat2.isChecked():
data["Bat2_Cyc"] = self.inp_bat2_cyc.text()
data["Bat2_Cap"] = self.inp_bat2_cap.text()
self.settings.setValue("sheet_url", url)
try:
manager = SheetManager(url, self.cfg)
res = manager.send_test_results(data)
QMessageBox.information(self, "Готово", res)
except Exception as e:
QMessageBox.critical(self, "Ошибка отправки", str(e))
def main():
app = QApplication(sys.argv)
try:
cfg = load_config()
except (FileNotFoundError, ValueError) as e:
QMessageBox.critical(None, "Ошибка конфигурации", str(e))
sys.exit(1)
window = TesterApp(cfg)
window.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()