mirror of
https://github.com/Horizontal42/LaptopTester.git
synced 2026-11-10 18:22:01 +03:00
Compare commits
No commits in common. "v1.5" and "master" have entirely different histories.
5 changed files with 221 additions and 11 deletions
7
.gitignore
vendored
7
.gitignore
vendored
|
|
@ -15,15 +15,8 @@ settings.ini
|
|||
tools/*
|
||||
!tools/README.md
|
||||
|
||||
# internal/AI tool files
|
||||
ТЕст/
|
||||
.claude/
|
||||
CLAUDE.md
|
||||
*.md.bak
|
||||
.aider*
|
||||
bugs.md
|
||||
*_ai_notes*
|
||||
llm_context*
|
||||
|
||||
# PyInstaller leftovers
|
||||
*.spec.bak
|
||||
|
|
|
|||
107
ARCHITECTURE.md
Normal file
107
ARCHITECTURE.md
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
# LaptopTester Architecture
|
||||
|
||||
## Files
|
||||
|
||||
```
|
||||
config.py # Load sheet_config.json, validate, expose AppConfig dataclass
|
||||
scanner.py # WMI/smartctl hardware detection in QThread; matching logic
|
||||
sheet_manager.py # Google Sheets auth (service account or OAuth) and row upsert
|
||||
tester.py # PyQt5 GUI: pages, QLineEdit/QComboBox fields, thread signals
|
||||
sheet_config.example.json # Template: worksheet, column numbers, hardware lists, brand aliases
|
||||
requirements.txt # Dependencies: PyQt5, WMI, gspread, google-auth libs
|
||||
LaptopTester.spec # PyInstaller config (datas, hidden imports)
|
||||
```
|
||||
|
||||
## How hardware scan flows
|
||||
|
||||
1. User starts app → `TesterApp.__init__` checks admin rights, builds UI (3 pages: load, hardware, tests)
|
||||
2. `start_scan()` spawns `HardwareScanner` QThread with `AppConfig`
|
||||
3. Scanner queries WMI for brand, model, CPU, RAM; smartctl for SSD SMART data
|
||||
4. `find_best_match(raw_wmi_name, config_list)` normalizes names and tokens to match against lists
|
||||
- Exact normalized match wins
|
||||
- Falls back to longest list entry whose tokens all appear in raw name
|
||||
- For GPU entries like "NVIDIA MX: MX450", also tries matching only the model part (after `:`)
|
||||
5. Scanner emits `log()` signal → progress label updates; emits `finished(dict)` → `on_scan_finished()`
|
||||
6. `on_scan_finished()` populates combo boxes: finds matching list entry, calls `setCurrentIndex()`
|
||||
|
||||
## How results flow to Google Sheets
|
||||
|
||||
1. User fills hardware page, checks tests, clicks "ОТПРАВИТЬ"
|
||||
2. `finish_testing()` gathers all form data into dict
|
||||
3. If second battery group checked, adds `Bat2_Cyc`/`Bat2_Cap`
|
||||
4. Validates serial number (non-empty) — shows warning if missing
|
||||
5. Creates `SheetManager(url, cfg)` → connects to sheet (service account or OAuth)
|
||||
6. Calls `send_test_results(data_dict)`:
|
||||
- Finds row by serial number in `cfg.serial_column`
|
||||
- If found: updates; if not found: appends after last filled row
|
||||
- Iterates config columns: for each key in `cfg.columns`, writes corresponding data value
|
||||
7. Returns status message, shows in QMessageBox
|
||||
|
||||
## Adding new hardware checks
|
||||
|
||||
1. Edit `sheet_config.example.json` `lists.gpus` (or other list)
|
||||
2. Add brand alias if needed: `brand_aliases: {"Raw Name": "Canonical"}`
|
||||
3. Restart app — scanner uses new lists
|
||||
|
||||
To add a _new type_ of check (e.g., WiFi module):
|
||||
|
||||
1. Add to `sheet_config.example.json` columns: `"WiFi": 51` (pick next free column)
|
||||
2. Add to `tester.py` `init_tests_ui()`:
|
||||
```python
|
||||
add(17, "WiFi", ["+", "-", "Не инфы"], "🌐", None)
|
||||
```
|
||||
3. Add to `finish_testing()` data dict:
|
||||
```python
|
||||
"WiFi": self.checks["WiFi"].currentText(),
|
||||
```
|
||||
4. Update example config to include in `columns` so it gets written
|
||||
|
||||
## Adding new hardware detection
|
||||
|
||||
1. Add field to `scanner.py` `HardwareScanner.run()`:
|
||||
```python
|
||||
self.log.emit("New hardware...")
|
||||
data["NewField"] = self.detect_new_field()
|
||||
```
|
||||
2. Add corresponding column to `sheet_config.example.json`
|
||||
3. Add combo box or text field to `tester.py` hardware page
|
||||
4. Add to data dict in `finish_testing()`
|
||||
|
||||
## Gotchas
|
||||
|
||||
- **WMI names are messy:** "NVIDIA GeForce RTX 3060" vs config "NVIDIA RTX: 3060". Token matching handles this, but config lists must be carefully curated (ditch duplicates like both "GeForce GT 620M" and "NVIDIA GT: GT 620M").
|
||||
- **SSD sizes:** Drives report decimal GB (1 TB = 1e12 bytes), but older `int(bytes / 1024**3)` gave binary GiB. Now matches by closest size ±7%, so 240 GB at 256e9 bytes finds "240 GB" correctly.
|
||||
- **AMD Ryzen format:** WMI says "Ryzen 5 3500U", config lists use "R5 3500U". Scanner normalizes "Ryzen X" → "RX" before matching.
|
||||
- **Column order matters:** `sheet_config.json` `columns` dict values are 1-indexed Google Sheets column numbers. Serial column is `serial_column` (used for find/upsert logic), separate from `columns` map.
|
||||
- **Rescan:** Once started, `HardwareScanner` thread blocks until done. If user clicks Rescan again before done, old thread keeps running (potential race). Check `scanner.isRunning()` before starting new one, or emit a signal to prevent UI clicks.
|
||||
- **Password reset:** `net accounts /maxpwage:unlimited` changes _system state_. Runs silently in background; returncode 0 = success. Needs admin rights or fails silently.
|
||||
- **Empty serial number:** `sheet_manager` checks for non-empty serial; if empty, returns error string (not written to sheet). Caller (`tester.py`) should validate before calling.
|
||||
|
||||
## Config format
|
||||
|
||||
`sheet_config.json` structure:
|
||||
```json
|
||||
{
|
||||
"worksheet": "Laptops",
|
||||
"serial_column": 21,
|
||||
"columns": {
|
||||
"Brand": 19,
|
||||
"CPU": 22,
|
||||
...
|
||||
},
|
||||
"brand_aliases": {
|
||||
"Hewlett-Packard": "HP",
|
||||
...
|
||||
},
|
||||
"lists": {
|
||||
"brands": ["HP", "Lenovo", ...],
|
||||
"models": [...],
|
||||
"cpus": [...],
|
||||
"ram": [...],
|
||||
"ssd": [...],
|
||||
"gpus": ["", "NVIDIA RTX: 3060", ...]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Empty string in GPU list = "no dedicated GPU" (matches Intel/AMD integrated).
|
||||
109
ARCHITECTURE.ru.md
Normal file
109
ARCHITECTURE.ru.md
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
[English](ARCHITECTURE.md)
|
||||
|
||||
# LaptopTester: архитектура
|
||||
|
||||
## Файлы
|
||||
|
||||
```
|
||||
config.py # Загрузка sheet_config.json, валидация, AppConfig dataclass
|
||||
scanner.py # Определение железа (WMI/smartctl) в QThread; логика матчинга
|
||||
sheet_manager.py # Google Sheets auth (service account или OAuth) и запись строк
|
||||
tester.py # PyQt5 GUI: страницы, QLineEdit/QComboBox, сигналы потока
|
||||
sheet_config.example.json # Шаблон: имя листа, номера столбцов, списки железа, алиасы брендов
|
||||
requirements.txt # Зависимости: PyQt5, WMI, gspread, google-auth
|
||||
LaptopTester.spec # Конфиг PyInstaller (datas, hidden imports)
|
||||
```
|
||||
|
||||
## Поток сканирования железа
|
||||
|
||||
1. Пользователь запускает приложение → `TesterApp.__init__` проверяет права админа, строит UI (3 страницы: загрузка, железо, тесты)
|
||||
2. `start_scan()` создаёт `HardwareScanner` QThread с `AppConfig`
|
||||
3. Scanner запрашивает WMI (бренд, модель, CPU, RAM) и smartctl (SMART данные SSD)
|
||||
4. `find_best_match(raw_wmi_name, config_list)` нормализует имена и токены для сопоставления
|
||||
- Точное совпадение нормализованного названия побеждает
|
||||
- Fallback: самый длинный элемент из списка, чьи токены всё содержатся в сыром имени
|
||||
- Для GPU типа "NVIDIA MX: MX450" также пытается матчить только часть после `:`
|
||||
5. Scanner отправляет `log()` сигнал → обновляется label прогресса; отправляет `finished(dict)` → вызывает `on_scan_finished()`
|
||||
6. `on_scan_finished()` заполняет комбо-боксы: находит совпадающий элемент списка, вызывает `setCurrentIndex()`
|
||||
|
||||
## Поток передачи результатов в Google Sheets
|
||||
|
||||
1. Пользователь заполняет страницу железа, отмечает тесты, нажимает "ОТПРАВИТЬ"
|
||||
2. `finish_testing()` собирает все данные формы в dict
|
||||
3. Если включена группа второй батареи, добавляет `Bat2_Cyc`/`Bat2_Cap`
|
||||
4. Валидирует серийный номер (не пустой) — показывает warning если пуст
|
||||
5. Создаёт `SheetManager(url, cfg)` → подключается к таблице (service account или OAuth)
|
||||
6. Вызывает `send_test_results(data_dict)`:
|
||||
- Ищет строку по серийному номеру в `cfg.serial_column`
|
||||
- Если найдена: обновляет; если не найдена: добавляет после последней заполненной
|
||||
- Итерирует столбцы из конфига: для каждого ключа в `cfg.columns` пишет соответствующее значение
|
||||
7. Возвращает сообщение статуса, показывает в QMessageBox
|
||||
|
||||
## Добавление нового теста железа
|
||||
|
||||
1. Отредактируй `sheet_config.example.json` раздел `lists.gpus` (или другой список)
|
||||
2. Добавь алиас бренда если нужно: `brand_aliases: {"Сырое имя": "Каноническое"}`
|
||||
3. Перезапусти приложение — scanner использует новые списки
|
||||
|
||||
Чтобы добавить _новый тип_ проверки (например, WiFi):
|
||||
|
||||
1. Добавь в `sheet_config.example.json` столбец: `"WiFi": 51` (выбери свободный номер)
|
||||
2. Добавь в `tester.py` функцию `init_tests_ui()`:
|
||||
```python
|
||||
add(17, "WiFi", ["+", "-", "Нет инфы"], "🌐", None)
|
||||
```
|
||||
3. Добавь в `finish_testing()` в data dict:
|
||||
```python
|
||||
"WiFi": self.checks["WiFi"].currentText(),
|
||||
```
|
||||
4. Обнови example-конфиг чтобы включить в `columns` — тогда будет писаться в таблицу
|
||||
|
||||
## Добавление нового определения железа
|
||||
|
||||
1. Добавь поле в `scanner.py` метод `HardwareScanner.run()`:
|
||||
```python
|
||||
self.log.emit("New hardware...")
|
||||
data["NewField"] = self.detect_new_field()
|
||||
```
|
||||
2. Добавь соответствующий столбец в `sheet_config.example.json`
|
||||
3. Добавь комбо-бокс или текстовое поле в `tester.py` страницу железа
|
||||
4. Добавь в data dict в `finish_testing()`
|
||||
|
||||
## Хитрости
|
||||
|
||||
- **WMI имена грязные:** "NVIDIA GeForce RTX 3060" vs конфиг "NVIDIA RTX: 3060". Токенный матчинг это решает, но списки конфига нужно тщательно курировать (убирать дубликаты как "GeForce GT 620M" и "NVIDIA GT: GT 620M" одновременно).
|
||||
- **Размеры SSD:** Диски报告ют десятичный GB (1 TB = 1e12 байт), но старый `int(bytes / 1024**3)` давал двоичный GiB. Теперь матчим по ближайшему размеру ±7%, так что 240 GB при 256e9 байт находит "240 GB" корректно.
|
||||
- **AMD Ryzen формат:** WMI говорит "Ryzen 5 3500U", конфиг-списки используют "R5 3500U". Scanner нормализует "Ryzen X" → "RX" перед матчингом.
|
||||
- **Порядок столбцов важен:** `sheet_config.json` ключи `columns` — это 1-индексированные номера столбцов Google Sheets. Столбец серийника — `serial_column` (используется для поиска/обновления), отдельно от карты `columns`.
|
||||
- **Рескан:** Запущенный `HardwareScanner` поток блокирует UI до завершения. Если пользователь кликнет Рескан до конца, старый поток продолжает работать (potential race). Проверь `scanner.isRunning()` перед стартом нового, или заблокируй клик кнопки сигналом.
|
||||
- **Сброс пароля:** `net accounts /maxpwage:unlimited` изменяет _состояние системы_. Выполняется молча в фоне; returncode 0 = успех. Нужны права админа или падает молча.
|
||||
- **Пустой серийник:** `sheet_manager` проверяет на непустоту; если пуст, возвращает строку-ошибку (не пишется в таблицу). Вызывающий код (`tester.py`) должен валидировать перед вызовом.
|
||||
|
||||
## Формат конфигурации
|
||||
|
||||
Структура `sheet_config.json`:
|
||||
```json
|
||||
{
|
||||
"worksheet": "Laptops",
|
||||
"serial_column": 21,
|
||||
"columns": {
|
||||
"Brand": 19,
|
||||
"CPU": 22,
|
||||
...
|
||||
},
|
||||
"brand_aliases": {
|
||||
"Hewlett-Packard": "HP",
|
||||
...
|
||||
},
|
||||
"lists": {
|
||||
"brands": ["HP", "Lenovo", ...],
|
||||
"models": [...],
|
||||
"cpus": [...],
|
||||
"ram": [...],
|
||||
"ssd": [...],
|
||||
"gpus": ["", "NVIDIA RTX: 3060", ...]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Пустая строка в списке GPU = "нет дискретной видеокарты" (матчится с Intel/AMD встроенными).
|
||||
2
LICENSE
2
LICENSE
|
|
@ -1,6 +1,6 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2024
|
||||
Copyright (c) 2026 Andrew Pimenov
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
# -*- mode: python ; coding: utf-8 -*-
|
||||
|
||||
|
||||
a = Analysis(
|
||||
['tester.py'],
|
||||
pathex=[],
|
||||
binaries=[],
|
||||
datas=[],
|
||||
hiddenimports=[],
|
||||
datas=[
|
||||
('sheet_config.example.json', '.'),
|
||||
],
|
||||
hiddenimports=['wmi', 'win32com', 'win32com.client'],
|
||||
hookspath=[],
|
||||
hooksconfig={},
|
||||
runtime_hooks=[],
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue