update copyright year to 2026, add architecture docs

This commit is contained in:
Andrew Pimenov 2026-06-13 19:14:20 +03:00
parent 1925166988
commit 1ce560f13a
3 changed files with 217 additions and 1 deletions

107
ARCHITECTURE.md Normal file
View 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
View 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 встроенными).

View file

@ -1,6 +1,6 @@
MIT License MIT License
Copyright (c) 2024 Andrew Pimenov Copyright (c) 2026 Andrew Pimenov
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal