From 5051dfdc8c954a9243ddfa2b3b9d6d3c8883b95a Mon Sep 17 00:00:00 2001 From: Scott Scoble Date: Thu, 14 May 2026 17:40:04 -0700 Subject: [PATCH] Add T490 hardware configuration and docs - modules/t490.nix: TLP power management, CPU throttling (throttled), S3 sleep, remote Nix builders (cluster), fwupd, fprintd, fprintd, Syncthing, Tailscale, Intel VA-API, TrackPoint sensitivity, PipeWire defaults - flake.nix: add nixos-hardware input, import T490 module + nixos-hardware - docs/T490-HARDWARE.md: WiFi variants, graphics, audio, fingerprint, sleep guide - docs/T490-BATTERY.md: TLP config, threshold strategy, real-world battery numbers - docs/T490-OFFLOADING.md: local vs cluster split, remote builder setup - MIGRATION.md: add T490 post-install steps (fwupd, fprint, remote build key) - README.md: reflect new module and docs structure --- MIGRATION.md | 26 +++++++ README.md | 11 ++- docs/T490-BATTERY.md | 56 ++++++++++++++ docs/T490-HARDWARE.md | 63 ++++++++++++++++ docs/T490-OFFLOADING.md | 49 +++++++++++++ flake.nix | 5 +- modules/t490.nix | 157 ++++++++++++++++++++++++++++++++++++++++ 7 files changed, 364 insertions(+), 3 deletions(-) create mode 100644 docs/T490-BATTERY.md create mode 100644 docs/T490-HARDWARE.md create mode 100644 docs/T490-OFFLOADING.md create mode 100644 modules/t490.nix diff --git a/MIGRATION.md b/MIGRATION.md index d10bd51..acf4f9b 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -88,6 +88,27 @@ curl -s http://localhost:3987/health # should return ok # Discord: @mention Marcus in #speaker-site ``` +### 8. T490 hardware setup +```bash +# Firmware updates (BIOS, EC, Thunderbolt, fingerprint reader) +fwupdmgr refresh && fwupdmgr update +# May need to disable Boot Order Lock in BIOS first + +# Enroll fingerprint +fprintd-enroll + +# Check sleep mode +cat /sys/power/mem_sleep +# Should show [deep] if S3 is available and configured + +# Set up remote build SSH key +sudo ssh-keygen -t ed25519 -f /root/.ssh/remote-build -N "" +# Copy /root/.ssh/remote-build.pub to each cluster node's /root/.ssh/authorized_keys + +# Set up Syncthing (pair with cluster nodes for vault sync) +systemctl --user enable --now syncthing +``` + ## Files to NOT transfer These contain caches, build artifacts, or macOS-specific junk: - `~/Library/` — macOS only @@ -113,3 +134,8 @@ These contain caches, build artifacts, or macOS-specific junk: - [ ] Hyprland: SUPER+D opens rofi, SUPER+Return opens ghostty+zellij - [ ] Discord: @mention Marcus → gets a reply - [ ] Wallpaper appears after login +- [ ] Firmware updated (`fwupdmgr update`) +- [ ] Fingerprint enrolled (`fprintd-enroll`) +- [ ] S3 sleep working (`cat /sys/power/mem_sleep` shows `[deep]`) +- [ ] Remote Nix builders working (`nix build --rebuild nixpkgs#hello`) +- [ ] Syncthing running for vault sync diff --git a/README.md b/README.md index 7a149bb..633a0d0 100644 --- a/README.md +++ b/README.md @@ -131,11 +131,17 @@ laptop-config/ ├── flake.nix # Two targets: nixosConfigurations.laptop + homeConfigurations.sscoble ├── modules/ │ ├── laptop.nix # System packages, zsh, fonts, SSH -│ └── hyprland.nix # Hyprland WM, greetd, PipeWire, portals, bluetooth +│ ├── hyprland.nix # Hyprland WM, greetd, PipeWire, portals, bluetooth +│ ├── t490.nix # T490 hardware: TLP, throttled, S3 sleep, remote builders, fwupd +│ └── vm.nix # UTM aarch64 VM for testing ├── home/ -│ ├── sscoble.nix # Shared (macOS + NixOS): zsh, git, nvim, pi, dirs, SSH +│ ├── sscoble.nix # Shared (macOS + NixOS): zsh, git, nvim, pi, direnv, SSH │ ├── desktop.nix # Linux-only: Hyprland, waybar, ghostty, zellij, themes │ └── wgu.nix # WGU software: Zotero, Brave, Obsidian, VSCode, Typst, Bitwarden +├── docs/ +│ ├── T490-HARDWARE.md # WiFi, GPU, audio, fingerprint, sleep/resume guide +│ ├── T490-BATTERY.md # TLP config, threshold strategy, real-world numbers +│ └── T490-OFFLOADING.md # What runs local vs cluster, remote builder setup └── README.md ``` @@ -157,6 +163,7 @@ Same zsh, git, neovim, pi config, SSH config on both machines. |------|--------| | [sscoble/pi-mono](https://os.scoble.me/forgejo/sscoble/pi-mono) | pi coding agent (mirror of badlogic/pi-mono) | | [sscoble/wgu](https://os.scoble.me/forgejo/sscoble/wgu) | WGU coursework monorepo — class templates, study strategy, OB1 integration | +| [NixOS/nixos-hardware](https://github.com/NixOS/nixos-hardware) | T490 hardware quirks module (imported via flake input) | ## Forgejo Actions diff --git a/docs/T490-BATTERY.md b/docs/T490-BATTERY.md new file mode 100644 index 0000000..0c8e438 --- /dev/null +++ b/docs/T490-BATTERY.md @@ -0,0 +1,56 @@ +# T490 Battery Life + +## Hardware Baseline + +- **50 Wh** internal battery (3-cell) +- Panel draws ~4-5W at typical brightness +- CPU TDP-up 25W, TDP-down 10W + +## Real-World Estimates + +| Scenario | Runtime | +|---|---| +| Idle, screen off | 10-12 hours | +| Light work (terminal, notes) | 7-8 hours | +| Dev work (editor, browser, kubectl) | 5-6 hours | +| Browsing (10+ tabs, video) | 4-5 hours | +| Compiling / heavy CPU | 1.5-2 hours | + +## TLP Configuration + +`modules/t490.nix` configures: +- `powersave` governor on battery, `performance` on AC +- Turbo disabled on battery +- CPU capped at 40% max frequency on battery (~1.7 GHz) +- WiFi power saving on battery +- PCIe runtime PM (auto on battery) +- Intel GPU frequency limits (300-600 MHz on battery) + +### Battery Health Thresholds + +Lenovo Battery Team recommendations — configured in `modules/t490.nix`: +- **Start charging** below 40% +- **Stop charging** at 80% +- Thresholds persist in firmware (survive reboots without TLP) + +This dramatically extends battery lifespan. Don't leave plugged in at 100% for days. + +## CPU Throttling Fix + +Intel Whiskey Lake CPUs have a bug where BD PROCHOT fires spuriously, locking the CPU at 400 MHz. The nixos-hardware T490 module enables `services.throttled` (erpalma/throttled) — this is mandatory. + +**Skip thermald** — it conflicts with throttled. `services.thermald.enable = false` is set in `modules/t490.nix`. + +## Additional Tweaks + +- `pcie_aspm=force` — force PCIe power saving +- `i915.enable_rc6=7` — max GPU power saving +- `noatime,nodiratime` — reduce disk writes +- **zram swap** at 50% of RAM — faster, saves NVMe writes + +## Quick Wins (No Config Needed) + +- Lower screen brightness (40-60% is plenty — biggest power draw) +- Disable Bluetooth when not in use +- Use S3 deep sleep (not s2idle) +- Close unused browser tabs diff --git a/docs/T490-HARDWARE.md b/docs/T490-HARDWARE.md new file mode 100644 index 0000000..4c53066 --- /dev/null +++ b/docs/T490-HARDWARE.md @@ -0,0 +1,63 @@ +# T490 Hardware Compatibility + +## WiFi + +Two possible cards depending on SKU: +- **Intel Wireless-AC 9560** (CNVio, older) — firmware: `iwlwifi-9000-pu-b0-jf-b0-*.ucode` +- **Intel Wi-Fi 6 AX200** (M.2, discrete, newer) — firmware: `iwlwifi-cc-a0-*.ucode` + +Both use `iwlwifi` and work out of the box. If AX200 scan fails after suspend, firmware version may be the culprit. The `modules/t490.nix` includes a resume hook that reloads the module. + +## Graphics — Intel UHD 620 (Whiskey Lake) + +Single iGPU. No NVIDIA. No Optimus. Ideal for Linux. + +| Feature | Support | +|---|---| +| OpenGL 4.6 | Mesa Iris driver | +| Vulkan 1.3 | Mesa ANV driver | +| VA-API decode | H.264, H.265 8-bit, VP9 8+10-bit, MPEG-2, VC-1, VP8 | +| VA-API encode | H.264 only (no H.265) | +| DisplayPort over USB-C | Up to 4096×2304 @ 60Hz | +| HDMI 1.4b | Up to 4096×2304 @ 24Hz | + +## Thunderbolt 3 + +One full-speed TB3 port (also USB-C PD). `services.hardware.bolt.enable = true` in `modules/t490.nix`. Lenovo TB3 Dock Gen 2 works — update dock firmware via fwupd. + +## Fingerprint Reader — Synaptics 06cb:00bd + +Works after firmware update. Steps: +1. `fwupdmgr refresh && fwupdmgr update` — flashes Prometheus firmware +2. `fprintd-enroll` — enroll fingerprints +3. Works for sudo, login, and screen unlock via PAM + +Check: `lsusb | grep 06cb` should show `06cb:00bd`. + +## Audio + +Single Intel audio card (`8086:9dc8`) with 5 sub-devices (speaker + 4 HDMI/USB-C). The nixos-hardware T490 module provides a custom PulseAudio profile set that exposes all 5 sinks simultaneously. + +## Sleep / Resume — Critical + +The T490 supports S3 (deep sleep) and S0ix (s2idle, modern standby). Lenovo removed S3 from some BIOS versions. + +**Check:** +``` +cat /sys/power/mem_sleep +``` + +| Output | Status | Action | +|---|---|---| +| `s2idle [deep]` | S3 default | Good | +| `[s2idle] deep` | S3 exists but not default | Already fixed in `modules/t490.nix` (`mem_sleep_default=deep`) | +| `[s2idle]` | S3 missing | Troubleshoot below | + +**If S3 is missing:** +1. BIOS → Config → Power → Sleep State → "Linux" (re-enables S3 on some versions) +2. Downgrade BIOS (pre-2020 versions have S3) +3. Use hibernation instead (more reliable than s2idle) + +**Sleep battery drain:** +- S3 (deep): ~0.5%/hour (~8 days on a 50 Wh battery) +- s2idle: ~2-4%/hour (1-2 days) diff --git a/docs/T490-OFFLOADING.md b/docs/T490-OFFLOADING.md new file mode 100644 index 0000000..c9fc7af --- /dev/null +++ b/docs/T490-OFFLOADING.md @@ -0,0 +1,49 @@ +# T490 Offloading Strategy + +The k3s cluster (n97, n97-mini) handles CPU-bound work so the T490 stays cool and battery lasts longer. + +## Local (T490) + +Interactive, latency-sensitive, or IO-bound: +- Firefox (web apps, research) +- Terminal + shell (`ghostty` + `zellij`) +- Editor (`nvim`) +- `kubectl` / `k9s` (just API calls) +- `git` on small repos +- `nixos-rebuild switch` (builds go remote, CLI runs locally) +- SSH client +- Chat (Signal, Discord) +- Obsidian +- Image/document viewing + +## Cluster + +CPU-bound, batch, or service workloads: +- Heavy Nix derivations during `nixos-rebuild` → remote builders +- Go/Rust/C compilation → ssh/rsync build remotely +- Docker/OCI image builds → build where they run +- Package builds (CI) → Forgejo runners on cluster +- Data processing, ETL +- Kubeclaw agent operations +- Long-running services (Forgejo, registry, web services) + +## Remote Nix Builders + +Configured in `modules/t490.nix`: +```nix +nix.distributedBuilds = true; +nix.buildMachines = [ + { hostName = "n97-mini"; system = "x86_64-linux"; maxJobs = 4; ... } + { hostName = "n97"; system = "x86_64-linux"; maxJobs = 4; ... } +]; +``` + +**Setup:** `ssh-keygen -t ed25519 -f /root/.ssh/remote-build` on T490, copy pubkey to each builder. + +**When it works:** `nixos-rebuild switch --flake .#laptop` ships heavy derivations to the cluster. The T490 only builds what must be local (bootloader, activation scripts). + +## Dev Compilation Patterns + +- **sshfs mount:** Edit locally, files live on cluster → build via SSH +- **rsync + remote build:** Push source, build, pull binary back +- **nix develop with builders:** `nix develop .#project --builders "ssh://n97-mini"` diff --git a/flake.nix b/flake.nix index 380937a..3c3641a 100644 --- a/flake.nix +++ b/flake.nix @@ -11,9 +11,10 @@ url = "git+https://os.scoble.me/forgejo/sscoble/pi-mono.git"; flake = false; }; + nixos-hardware.url = "github:NixOS/nixos-hardware/master"; }; - outputs = { self, nixpkgs, home-manager, pi-mono, ... }@inputs: let + outputs = { self, nixpkgs, home-manager, pi-mono, nixos-hardware, ... }@inputs: let linuxSystem = "x86_64-linux"; darwinSystem = "aarch64-darwin"; @@ -66,6 +67,8 @@ home-manager.nixosModules.home-manager ./modules/laptop.nix ./modules/hyprland.nix + ./modules/t490.nix + nixos-hardware.nixosModules.lenovo-thinkpad-t490 { nixpkgs.config.allowUnfree = true; } { home-manager = { diff --git a/modules/t490.nix b/modules/t490.nix new file mode 100644 index 0000000..15d20a0 --- /dev/null +++ b/modules/t490.nix @@ -0,0 +1,157 @@ +{ config, pkgs, lib, ... }: + +{ + # === T490-specific hardware configuration === + # Requires nixos-hardware flake input: nixos-hardware.nixosModules.lenovo-thinkpad-t490 + # The nixos-hardware module provides: + # - services.throttled (CPU throttling fix) + # - PulseAudio profile set (5 simultaneous sinks) + # - common/cpu/intel (VA-API drivers, microcode) + + # === Kernel parameters === + boot.kernelParams = [ + "mem_sleep_default=deep" # prefer S3 deep sleep over s2idle + "pcie_aspm=force" # PCIe Active State Power Management + "i915.enable_fbc=1" # frame buffer compression (power saving) + "i915.enable_psr=1" # panel self-refresh (power saving) + "i915.enable_rc6=7" # maximum GPU render power saving + ]; + + boot.kernelModules = [ "thinkpad_acpi" ]; + boot.blacklistedKernelModules = [ + "iTCO_wdt" + "iTCO_vendor_support" + ]; + + # === Display DPI === + services.xserver.dpi = 157; # 14" 1920×1080 IPS = 157 DPI + + # === Firmware updates === + services.fwupd.enable = true; + + # === Fingerprint reader (Synaptics 06cb:00bd) === + # Requires firmware update via fwupd first + services.fprintd.enable = true; + + # === TLP — ThinkPad power management === + # nixos-hardware thinkpad module provides base config; tune here + services.tlp = { + enable = true; + settings = { + CPU_SCALING_GOVERNOR_ON_AC = "performance"; + CPU_SCALING_GOVERNOR_ON_BAT = "powersave"; + CPU_ENERGY_PERF_POLICY_ON_AC = "balance_performance"; + CPU_ENERGY_PERF_POLICY_ON_BAT = "power"; + CPU_MAX_PERF_ON_AC = 100; + CPU_MAX_PERF_ON_BAT = 40; # cap at ~1.7 GHz on battery + CPU_BOOST_ON_AC = 1; + CPU_BOOST_ON_BAT = 0; # disable turbo on battery + START_CHARGE_THRESH_BAT0 = 40; # battery health: start charging below 40% + STOP_CHARGE_THRESH_BAT0 = 80; # battery health: stop at 80% + WIFI_PWR_ON_AC = "off"; + WIFI_PWR_ON_BAT = "on"; + RUNTIME_PM_ON_AC = "on"; + RUNTIME_PM_ON_BAT = "auto"; + USB_AUTOSUSPEND = 1; + INTEL_GPU_MIN_FREQ_ON_AC = 300; + INTEL_GPU_MAX_FREQ_ON_AC = 1100; + INTEL_GPU_MIN_FREQ_ON_BAT = 300; + INTEL_GPU_MAX_FREQ_ON_BAT = 600; + }; + }; + + # === powertop === + powerManagement.powertop.enable = true; + + # Skip thermald — conflicts with throttled (enabled by nixos-hardware) + services.thermald.enable = false; + + # === zram swap (faster + saves NVMe writes) === + zramSwap = { + enable = true; + memoryPercent = 50; + }; + + # === Intel GPU — hardware video acceleration === + hardware.graphics = { + enable = true; + extraPackages = with pkgs; [ + intel-media-driver # modern VA-API (iHD) + intel-vaapi-driver # legacy VA-API (i965) + libvdpau-va-gl # VDPAU → VA-API bridge + ]; + }; + + environment.sessionVariables = { + LIBVA_DRIVER_NAME = "iHD"; + MOZ_ENABLE_WAYLAND = "1"; + }; + + # === Suspend/resume: reload WiFi after wake === + powerManagement.powerUpCommands = '' + ${pkgs.kmod}/bin/modprobe -r iwlmvm iwlwifi || true + ${pkgs.kmod}/bin/modprobe iwlwifi || true + ''; + + # === Remote Nix builders (k3s cluster) === + nix.distributedBuilds = true; + nix.buildMachines = [ + { + hostName = "n97-mini"; + system = "x86_64-linux"; + maxJobs = 4; + supportedFeatures = [ + "nixos-test" + "benchmark" + "big-parallel" + "kvm" + ]; + sshUser = "root"; + sshKey = "/root/.ssh/remote-build"; + } + { + hostName = "n97"; + system = "x86_64-linux"; + maxJobs = 4; + supportedFeatures = [ + "nixos-test" + "benchmark" + "big-parallel" + "kvm" + ]; + sshUser = "root"; + sshKey = "/root/.ssh/remote-build"; + } + ]; + + # === Syncthing (Obsidian vault sync) === + services.syncthing = { + enable = true; + user = "sscoble"; + dataDir = "/home/sscoble/.syncthing"; + configDir = "/home/sscoble/.syncthing/config"; + }; + + # === Tailscale (consistent cluster access from any network) === + services.tailscale.enable = true; + + # === NVMe tools === + environment.systemPackages = with pkgs; [ nvme-cli ]; + + # === Touchpad & TrackPoint === + services.xserver.libinput = { + enable = true; + touchpad = { + tapping = true; + naturalScrolling = true; + accelSpeed = "0.3"; + disableWhileTyping = true; + }; + }; + + # TrackPoint sensitivity + services.udev.extraRules = '' + ACTION=="add", SUBSYSTEM=="input", ATTR{name}=="*TrackPoint*", \ + ATTR{device/sensitivity}="200", ATTR{device/speed}="150" + ''; +}