Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,15 +121,15 @@ Users can always override AI agent version decisions:
- **Frontend**: React 18 with TypeScript, built with Vite, served in the Tauri WebView2
- **Styling**: CSS custom properties drive theming (dark/light), tokens in `src/app/theme.ts`
- **Backend**: Rust (Tauri 2), native ARM64 binary
- **Sensors**: Real telemetry via the native Windows PDH API (ACPI thermal zones, per-core load, live CPU frequency); CPU identity via the registry + `GetSystemInfo`
- **Sensors**: Real telemetry via the native Windows PDH API (ACPI thermal zones, per-core load, per-core-clock-derived Speed); CPU identity via the registry + `GetSystemInfo` (`sensors/identity.rs`), matched to a chip profile via layered detection in `sensors/chips.rs::match_profile` (SKU token → family/subfamily from name or Identifier → inferred SKU from core count + rated clock → honest family-only fallback → generic) — see `SENSORS.md` §6
- **State Management**: React hooks (useState, useEffect, useCallback)
- **Tray**: Exactly one tray icon, always (do NOT re-add the `app.trayIcon` block to `tauri.conf.json` — it duplicates the runtime-built icon). It always shows the single honest CPU temperature — there is no per-core sensor to drive a per-core/mode tray menu. The number is rendered with the native system font via GDI (`sensors/tray_render.rs`, Windows-only, behind a small `tray_icon_size()`/`render_number_rgba()` seam so a macOS menu-bar or Linux tray backend can implement the same two functions later).
- **Settings Persistence**: Tauri store plugin (JSON in app data folder)
## Key directories
- `src/app/` - TypeScript types, theme tokens, hooks (useSettings, useSensors)
- `src/components/` - React components (MenuBar, ProcessorInfo, TempTable, SettingsDialog, OverheatDialog, AboutDialog, MiniMode)
- `src-tauri/src/` - Rust backend (lib.rs = app wiring, sensors/ = telemetry)
- `src-tauri/src/sensors/` - Sensor providers (pdh.rs = primary, chips.rs = profiles, tray.rs = color/mode logic, tray_render.rs = Windows GDI digit rendering, types.rs = data shapes)
- `src-tauri/src/sensors/` - Sensor providers (pdh.rs = primary, identity.rs = raw CPU identity signals from the registry/topology, chips.rs = profiles + detection, tray.rs = color/mode logic, tray_render.rs = Windows GDI digit rendering, types.rs = data shapes)
- `tools/` - Phase 0 sensor probe scripts (PowerShell) + icon generator
## Real-data contract (CRITICAL)
- **All telemetry must be REAL.** No simulated, random, or fallback values anywhere.
Expand All @@ -141,8 +141,8 @@ Users can always override AI agent version decisions:
## Main window layout structure
The application displays all sensor information in a single window matching Core Temp's actual layout:
1. **Native title bar** — the OS draws it (icon, title, minimize/close); the window is decorated and opaque (no custom chrome, no transparency/blur). Dark/light native chrome follows the app theme via `getCurrentWindow().setTheme()`.
2. **Menu Bar** (`src/components/MenuBar.tsx`) — File (Exit) / Options (Settings, Overheat protection, Toggle Mini Mode, Always on top) / Tools (Refresh sensors) / Help (About ARMtemp). Rendered as themed HTML dropdowns (a native HMENU doesn't follow dark/light mode on Windows) styled to look like real Win32 menus. No unit toggle — Fahrenheit lives in Settings → Display. Launch flags `--settings` / `--overheat` / `--about` deep-link the dialogs.
3. **Select CPU** row (combo + `[N] Core(s) [N] Thread(s)` sunken count boxes) + **Processor Information** group box (Win32 etched border, sunken read-only value fields): Model / Platform / Frequency / CPUID full rows; `Boost | Lithography` and `Throttle | TDP` pairs. VID and Revision are intentionally omitted (permanently unavailable on Snapdragon X); Throttle is the live ACPI passive-limit status (red "Yes" while the firmware throttles).
2. **Menu Bar** (`src/components/MenuBar.tsx`) — File (Exit) / Options (Settings, Overheat protection, Toggle Mini Mode, Always on top) / Tools (Refresh sensors, Copy detection report) / Help (About ARMtemp). Rendered as themed HTML dropdowns (a native HMENU doesn't follow dark/light mode on Windows) styled to look like real Win32 menus. No unit toggle — Fahrenheit lives in Settings → Display. Launch flags `--settings` / `--overheat` / `--about` deep-link the dialogs. "Copy detection report" (`get_detection_report` command) copies a plain-text dump of every raw CPU identity signal plus how the chip was matched, for diagnosing misdetections on machines the maintainer doesn't own (see issue #2).
3. **Select CPU** row (combo + `[N] Core(s) [N] Thread(s)` sunken count boxes) + **Processor Information** group box (Win32 etched border, sunken read-only value fields): Model / Platform / Frequency / CPUID full rows; `Boost | Lithography` and `Throttle | TDP` pairs. VID and Revision are intentionally omitted (permanently unavailable on Snapdragon X); Throttle is the live ACPI passive-limit status (red "Yes" while the firmware throttles). CPUID shows the real registry `Identifier` string (e.g. "ARMv8 (64-bit) Family 8 Model 2 Revision 201"), not a repeat of the Model field's marketing string.
4. **Temperature Readings** group box — Tj. Max row, one **CPU Temp** row (Cur. | Min. | Max. | Avg., **colored temperature text** — the app's single honest CPU temperature), then per-core rows (Core # | Load | Min. | Max. | Avg., plain text — genuinely per-core).
5. **Status Bar** — thin native strip with CPU Temp / Low / High (session, since app start).
6. **Mini-mode** drops native decorations at runtime (`setDecorations(false)` + `setSize()`) for a compact always-on-top box, and restores them on exit — matches Core Temp's mini mode.
Expand Down
34 changes: 29 additions & 5 deletions SENSORS.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,11 +141,35 @@ failure described in §7.
single CPU temperature ARMtemp displays (there is no per-core sensor to report
individually).
- **`Processor Information`** — `% Processor Time` per core (instances named
`group,core`, e.g. `0,3`) and `Processor Frequency` on `_Total` — a genuinely **live**
frequency, unlike `Win32_Processor.CurrentClockSpeed` (§1 note below).
- **Registry + `GetSystemInfo`** — CPU name (`HKLM\HARDWARE\DESCRIPTION\System\
CentralProcessor\0\ProcessorNameString`) and logical core count, used to auto-select
the chip profile for labelling.
`group,core`, e.g. `0,3`), `% Processor Performance` per core, and `Processor
Frequency` on `_Total`. **Speed** is computed per-core (`% Processor Performance ×
that core's registry `~MHz`) and the fastest core wins — the `_Total` counter alone
blends both P/E clusters into a meaningless average on a heterogeneous chip (see
issue #2: an idle Efficiency cluster dragged a busy X2's reported Speed well below
its real Prime-cluster clock); `_Total` is kept only as a fallback if the per-core
counters aren't available.
- **Registry + `GetSystemInfo`** (`sensors/identity.rs`) — `ProcessorNameString`,
`Identifier` (MIDR-derived: "Model 1" = 1st-gen Oryon/X1, "Model 2" = Oryon V3/X2),
`VendorIdentifier`, per-core `~MHz` (each core's cluster rated/boost clock — the same
value CPU-Z's "Original Processor Frequency" and HWiNFO read), and logical core count.
- **Chip detection** (`sensors/chips.rs::match_profile`) is layered, since issue #2
showed some OEM firmware (Surface, Snapdragon X2 Elite) reports a bare marketing name
with no SKU token in it at all — token matching alone silently produced "Unknown X2
Elite SKU" for it every time:
1. Exact SKU token in the name (e.g. `X2E78100`) — used when present.
2. Family/subfamily from the name text ("X2 Elite", "X2 Elite Extreme", "X2 Plus", …),
or — if the name has no usable text — the registry `Identifier`'s Oryon generation
+ a Qualcomm vendor check.
3. Within that family, the exact SKU inferred from real core count + real rated clock
(`~MHz`) — nearest-clock match within a tolerance, since every family+core-count
group's neighboring SKUs are ≥300 MHz apart. X2E-80-100 and X2E-84-100 publish
identical CPU-visible specs (same 4.7 GHz boost, cache, core split) and are
reported as one honest combined "X2E-80/84-100" label rather than a guess.
4. Family recognized but no SKU candidate fits (unreleased part): label the family
honestly with `boost_ghz` from the real measured clock — never "Generic".
5. Nothing Snapdragon-shaped: generic fallback.
Real P/E core counts from the OS topology query (below) override any static table
cluster split whenever they're available and consistent with the detected core count.
- **Power** — not wired; confirmed unavailable from userspace (§3).
- Emit a single `sensor-update` Tauri event each tick with the merged snapshot.
- Strict real-only contract: any field with no real source is `None` → UI shows "—".
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "armtemp",
"private": true,
"version": "0.4.4",
"version": "0.4.5",
"type": "module",
"description": "Native Snapdragon X / X2 temperature monitor for Windows on ARM",
"scripts": {
Expand Down
2 changes: 1 addition & 1 deletion src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "armtemp"
version = "0.4.4"
version = "0.4.5"
description = "Native Snapdragon X / X2 temperature monitor for Windows on ARM"
authors = ["Spencer Francisco"]
edition = "2021"
Expand Down
19 changes: 16 additions & 3 deletions src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,17 @@ fn exit_app(app: tauri::AppHandle) {
app.exit(0);
}

/// Plain-text dump of every raw CPU identity signal plus how the chip was
/// matched — see issue #2, where the only way to diagnose a misdetected
/// machine we don't own was asking the reporter for more screenshots.
#[tauri::command]
fn get_detection_report(state: tauri::State<'_, Arc<AppState>>) -> String {
state
.provider
.detection_report()
.unwrap_or_else(|e| format!("detection report unavailable: {e}"))
}

/// Keeps the tray menu's "Mini-mode" checkmark honest. Mini mode is ephemeral
/// UI state (not persisted settings), so the frontend reports it here on
/// every toggle rather than routing it through `update_settings`.
Expand Down Expand Up @@ -609,8 +620,9 @@ pub fn run() {
let provider = PdhProvider::new();
let profile = provider.profile().ok().flatten();
eprintln!(
"[armtemp] detected: {}",
profile.as_ref().map(|p| p.name).unwrap_or("unknown")
"[armtemp] detected: {} {}",
profile.as_ref().map(|p| p.name).unwrap_or("unknown"),
profile.as_ref().map(|p| p.model).unwrap_or("")
);
let state = Arc::new(AppState {
provider,
Expand Down Expand Up @@ -694,7 +706,8 @@ pub fn run() {
refresh_now,
update_settings,
exit_app,
set_mini_state
set_mini_state,
get_detection_report
])
.run(tauri::generate_context!())
.expect("error while running ARMtemp");
Expand Down
Loading
Loading