mirror of
https://github.com/Horizontal42/LaptopTester.git
synced 2026-11-10 16:02:05 +03:00
109 lines
7.6 KiB
Markdown
109 lines
7.6 KiB
Markdown
[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 встроенными).
|