mirror of
https://github.com/Horizontal42/LaptopTester.git
synced 2026-11-10 16:02:05 +03:00
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
299 lines
11 KiB
Python
299 lines
11 KiB
Python
# 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()
|