LaptopTester/ARCHITECTURE.md
2026-06-13 19:14:20 +03:00

4.9 KiB
Raw Permalink Blame History

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():
    add(17, "WiFi", ["+", "-", "Не инфы"], "🌐", None)
    
  3. Add to finish_testing() data dict:
    "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():
    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:

{
  "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).