diff --git a/22.04-OEM-C.md b/22.04-OEM-C.md index b78312d..032693f 100644 --- a/22.04-OEM-C.md +++ b/22.04-OEM-C.md @@ -1,12 +1,19 @@ -## For the alert box when linux-oem-22.04c receives an update +### OEM kernels are not needed at this time. -### A new OEM C kernel has been released, please re-run the command below to make sure you're on the latest Ubuntu 22.04 linux-oem-22.04c kernel. -- Browse to Activities in the upper left corner, click to open it. -- Type out the word terminal, click to open it. -- Left click and drag to highlight and copy the code below in the gray box, right click/paste to copy it into the terminal window. -- Then press the enter key, password, **reboot**. +``` +sudo nano /etc/default/grub +``` + +Make sure this line has the adddtional details removed to look like this: +``` +GRUB_CMDLINE_LINUX_DEFAULT="quiet splash" +``` +Then update grub, reboot. ``` -latest_oem_kernel=$(ls /boot/vmlinuz-* | awk -F"-" '{split($0, a, "-"); version=a[3]; if (version>max) {max=version; kernel=a[2] "-" a[3] "-" a[4]}} END{print kernel}') && sudo sed -i.bak '/^GRUB_DEFAULT=/c\GRUB_DEFAULT="Advanced options for Ubuntu>Ubuntu, with Linux '"$latest_oem_kernel"'"' /etc/default/grub && sudo update-grub +sudo update-grub ``` +if your grub configuration is still trying to use an OEM kernel and you would like to reset grub to it's conf defaults. + +Click here --->: [Bring grub back to default, use latest kernel installed.](https://github.com/FrameworkComputer/linux-docs/tree/main/ubuntu-kernel-switcher#bring-grub-back-to-default-use-latest-kernel-installed) diff --git a/22.04-OEM-D.md b/22.04-OEM-D.md index 70800eb..ee1735b 100644 --- a/22.04-OEM-D.md +++ b/22.04-OEM-D.md @@ -1,14 +1,19 @@ -### A new OEM D kernel has been released, please re-run the command below to make sure you're on the latest Ubuntu 22.04 linux-oem-22.04d kernel. - -- Browse to Activities in the upper left corner, click to open it. -- Type out the word terminal, click to open it. -- Left click and drag to highlight and copy the code below in the gray box, right click/paste to copy it into the terminal window. -- Then press the enter key, password, **reboot**. +### OEM kernels are not needed at this time. +``` +sudo nano /etc/default/grub +``` -Place holder for this as well: +Make sure this line has the adddtional details removed to look like this: +``` +GRUB_CMDLINE_LINUX_DEFAULT="quiet splash" +``` +Then update grub, reboot. ``` -latest_oem_kernel=$(ls /boot/vmlinuz-* | grep '6.5.0-10..-oem' | sort -V | tail -n1 | awk -F'/' '{print $NF}' | sed 's/vmlinuz-//') && sudo sed -i.bak '/^GRUB_DEFAULT=/c\GRUB_DEFAULT="Advanced options for Ubuntu>Ubuntu, with Linux '"$latest_oem_kernel"'"' /etc/default/grub && sudo update-grub && sudo apt install zenity && mkdir -p ~/.config/autostart && [ ! -f ~/.config/autostart/kernel_check.desktop ] && echo -e "[Desktop Entry]\nType=Application\nExec=bash -c \"latest_oem_kernel=\$(ls /boot/vmlinuz-* | grep '6.5.0-10..-oem' | sort -V | tail -n1 | awk -F'/' '{print \\\$NF}' | sed 's/vmlinuz-//') && current_grub_kernel=\$(grep '^GRUB_DEFAULT=' /etc/default/grub | sed -e 's/GRUB_DEFAULT=\\\"Advanced options for Ubuntu>Ubuntu, with Linux //g' -e 's/\\\"//g') && [ \\\"\\\${latest_oem_kernel}\\\" != \\\"\\\${current_grub_kernel}\\\" ] && zenity --text-info --html --width=300 --height=200 --title=\\\"Kernel Update Notification\\\" --filename=<(echo -e \\\"A newer OEM D kernel is available than what is set in GRUB. Click here to learn more.\\\")\"\nHidden=false\nNoDisplay=false\nX-GNOME-Autostart-enabled=true\nName[en_US]=Kernel check\nName=Kernel check\nComment[en_US]=\nComment=" > ~/.config/autostart/kernel_check.desktop +sudo update-grub ``` +if your grub configuration is still trying to use an OEM kernel and you would like to reset grub to it's conf defaults. + +Click here --->: [Bring grub back to default, use latest kernel installed.](https://github.com/FrameworkComputer/linux-docs/tree/main/ubuntu-kernel-switcher#bring-grub-back-to-default-use-latest-kernel-installed) diff --git a/Enhanced-WiFi-Analyzer/README.md b/Enhanced-WiFi-Analyzer/README.md new file mode 100644 index 0000000..9814d45 --- /dev/null +++ b/Enhanced-WiFi-Analyzer/README.md @@ -0,0 +1,340 @@ +# 🧠 Enhanced WiFi Analyzer + +> **Advanced WiFi diagnostics and troubleshooting for Linux with WiFi 7, DFS monitoring, and modern VPN support** + +[![Shell Script](https://img.shields.io/badge/Shell-Bash-green.svg)](https://www.gnu.org/software/bash/) +[![WiFi 7](https://img.shields.io/badge/WiFi-7%20Ready-blue.svg)]() +[![DFS Monitor](https://img.shields.io/badge/DFS-Monitoring-red.svg)]() + +A comprehensive WiFi analysis and troubleshooting tool that diagnoses connectivity issues, optimizes performance, and provides distribution-specific fixes for modern Linux systems. Features DFS radar interference detection, WiFi 7/6E support, and modern VPN integration. + +## ⚠️ Framework Support Disclaimer + +**Before implementing any power management changes recommended by this tool, please verify with Framework Support first.** + +While the Enhanced WiFi Analyzer provides valuable diagnostic information and generates safe configuration scripts, power management settings should only be modified when addressing specific connectivity issues. The tool may recommend disabling PCIe ASPM (Active State Power Management) or NetworkManager power saving features, but these changes should only be applied if: + +1. **You are experiencing actual WiFi connectivity problems** (disconnections, micro-dropouts, poor roaming) +2. **The analysis clearly identifies power management as the root cause** +3. **Framework Support has reviewed your specific situation and confirmed the recommendation** + +### Why This Matters + +Power management features exist for good reasons - they extend battery life and reduce heat generation. Disabling them unnecessarily can impact your system's efficiency without providing any benefits. The diagnostic tools help identify *potential* power management conflicts, but not every detection requires action. + +### Recommended Workflow + +1. **Run the analysis** to identify potential issues: Run the script per the instructions. +2. **Document your specific symptoms** (connection drops, poor performance, etc.) +3. **Contact Framework Support** with both your symptoms and the tool's findings +4. **Apply recommended changes only** after confirmation from Support +5. **Test thoroughly** and revert changes if they don't resolve your specific issues + +### Contact Framework Support + +[Contact](https://framework.kustomer.help/contact/support-request-ryon9uAuq) - Ask to send your findings to the Linux Support Team + +Remember: These diagnostic tools are designed to help identify issues, not automatically fix them. Always verify recommendations with Framework Support before making system changes. + +## 📚 Table of Contents + +- [🚀 Key Features](#-key-features) +- [🎯 Why Use This Tool?](#-why-use-this-tool) +- [📋 Quick Start](#-quick-start) +- [🎛️ Main Menu Options](#️-main-menu-options) +- [🔧 Common Use Cases](#-common-use-cases) +- [📊 Example Analysis Output](#-example-analysis-output) +- [🚀 Advanced Features](#-advanced-features) +- [💡 Pro Tips](#-pro-tips) +- [🔍 Troubleshooting Matrix](#-troubleshooting-matrix) +- [🛡️ Safety & Compatibility](#️-safety--compatibility) +- [📈 Future Development](#-future-development) +- [🔗 Related Tools](#-related-tools) +- [🤝 Contributing](#-contributing) + +## 🚀 Key Features + +### 🔬 **Advanced Diagnostics** +- **Complete WiFi Health Analysis** - Full system assessment with scoring +- **Modern Chipset Detection** - Supports WiFi 7, 6E, MLO, and new hardware +- **Real-time Connectivity Testing** - Tests actual data flow beyond basic connection status +- **Distribution-Aware Analysis** - Tailored for Fedora, Ubuntu, Arch (NetworkManager), Debian, openSUSE, and immutable systems + +### 📡 **DFS Radar Interference Monitoring** +- **Radar Event Detection** - Identifies DFS channel issues causing sudden disconnections +- **Smart Channel Switching** - Automated migration to non-DFS safe channels +- **Environment Analysis** - Maps DFS usage patterns in your area +- **6GHz Migration Path** - Recommendations for DFS-free 6GHz operation + +### 🔒 **Modern VPN Integration** +- **Advanced VPN Detection** - Supports Tailscale, ZeroTier, Nebula, WireGuard, commercial VPNs +- **VPN-WiFi Conflict Resolution** - Diagnoses and fixes VPN-related connectivity issues +- **Split Tunneling Optimization** - Configures optimal routing for modern mesh VPNs +- **MTU Optimization** - Automatic sizing for VPN tunnels + +### 🌐 **WiFi 6E/7 Optimization** +- **6GHz Band Analysis** - Clean spectrum identification and optimization +- **320MHz Channel Width** - Ultra-wide channel support for maximum throughput +- **MLO (Multi-Link Operation)** - WiFi 7 multi-band aggregation support +- **Regulatory Domain Optimization** - Proper power limits and channel availability + +### 🛠️ **Interactive Troubleshooting** +- **Guided Problem Solving** - Step-by-step fixes for common issues +- **Emergency Repair Mode** - Quick fixes for critical failures +- **Distribution-Specific Commands** - Tailored solutions for your Linux distribution +- **Thermal Management** - Overheating detection and fixes + +### ⚡ **Performance Optimization** +- **Band Switching Automation** - Intelligent 2.4/5/6GHz selection +- **Signal Strength Analysis** - RF environment mapping and optimization +- **Power Management** - Battery life vs performance balancing +- **Channel Width Optimization** - Maximizes throughput while maintaining stability + +## 🎯 Why Use This Tool? + +### **Solves Real Problems** +- **DFS Disconnections**: Identifies and fixes mysterious 30+ second WiFi drops caused by radar interference +- **Modern VPN Issues**: Resolves connectivity problems with Tailscale, ZeroTier, and other mesh VPNs +- **WiFi 7 Optimization**: Optimizes cutting-edge WiFi hardware within driver/firmware limitations +- **Distribution Chaos**: Provides correct commands for your specific Linux distribution + +### **Beyond Basic Tools** +- Most WiFi tools only check connection status - this analyzes actual data flow +- Detects issues that `nmcli` and GUI tools miss +- Provides root cause analysis, not just symptoms +- Includes proactive recommendations to prevent future issues + +### **Expert Knowledge Built-In** +- Incorporates knowledge of MediaTek, Intel, and Qualcomm chipset quirks +- Understands regulatory domain impacts on performance +- Knows which channels are safe vs DFS across different regions +- Includes thermal management strategies for high-performance WiFi cards + +## 📋 Quick Start + +### Prerequisites +```bash +# Required tools (install via package manager) +sudo apt install iw curl # Ubuntu/Debian +sudo dnf install iw curl # Fedora +sudo pacman -S iw curl # Arch (NetworkManager required - not iwd compatible) +``` + +### Installation +```bash +curl -s https://raw.githubusercontent.com/FrameworkComputer/linux-docs/refs/heads/main/Enhanced-WiFi-Analyzer/scripts/wifi_diagnostic.sh -o wifi_diagnostic.sh && clear && sudo bash wifi_diagnostic.sh +``` + +If already downloaded, just run: +```bash +sudo bash wifi_diagnostic.sh +``` + +### Quick Analysis +Download and run (first time): +```bash +curl -s https://raw.githubusercontent.com/FrameworkComputer/linux-docs/refs/heads/main/Enhanced-WiFi-Analyzer/scripts/wifi_diagnostic.sh -o wifi_diagnostic.sh && clear && sudo bash wifi_diagnostic.sh +``` + +If already downloaded: +```bash +sudo bash wifi_diagnostic.sh +``` + +Then choose your option: +- Option 1: Complete analysis (recommended first run) +- Option 4: DFS-specific monitoring for radar interference +- Option 9: Emergency fixes for immediate solutions + +## 🎛️ Main Menu Options + +| Option | Feature | Use Case | +|--------|---------|----------| +| **1** | 🎯 **Complete Analysis** | Full system health check with all modern features | +| **2** | 🚨 **Error Analysis** | Deep dive into logs and failure patterns | +| **3** | 🛠️ **Interactive Troubleshooting** | Guided problem-solving with custom solutions | +| **4** | 📡 **DFS Channel Monitor** | Dedicated radar interference analysis | +| **5** | 🧪 **TX Power Band Test** | Diagnose power limitations and optimize range | +| **6** | 📡 **Manual Band Switching** | Direct CLI commands for 2.4/5/6GHz control | + +## 🔧 Common Use Cases + +### **Scenario 1: Mysterious Disconnections** +Symptoms: WiFi drops for 30+ seconds randomly + +```bash +curl -s https://raw.githubusercontent.com/FrameworkComputer/linux-docs/refs/heads/main/Enhanced-WiFi-Analyzer/scripts/wifi_diagnostic.sh -o wifi_diagnostic.sh && clear && sudo bash wifi_diagnostic.sh +``` + +→ Choose Option 4 (DFS Monitor) +Tool identifies DFS radar interference and provides non-DFS channel solutions + +### **Scenario 2: Slow WiFi 7 Performance** +Symptoms: New WiFi 7 card performing poorly + +```bash +curl -s https://raw.githubusercontent.com/FrameworkComputer/linux-docs/refs/heads/main/Enhanced-WiFi-Analyzer/scripts/wifi_diagnostic.sh -o wifi_diagnostic.sh && clear && sudo bash wifi_diagnostic.sh +``` + +→ Choose Option 1 (Complete Analysis) +Tool detects 6GHz capability and recommends router upgrade/configuration + +### **Scenario 3: VPN Breaks WiFi** +Symptoms: WiFi unstable when Tailscale/ZeroTier active + +```bash +curl -s https://raw.githubusercontent.com/FrameworkComputer/linux-docs/refs/heads/main/Enhanced-WiFi-Analyzer/scripts/wifi_diagnostic.sh -o wifi_diagnostic.sh && clear && sudo bash wifi_diagnostic.sh +``` + +→ Choose Option 3 → Option 4 (VPN conflicts) +Tool provides MTU optimization and split tunneling configuration + +### **Scenario 4: Overheating Laptop** +Symptoms: High temperatures, fan noise during WiFi use + +```bash +curl -s https://raw.githubusercontent.com/FrameworkComputer/linux-docs/refs/heads/main/Enhanced-WiFi-Analyzer/scripts/wifi_diagnostic.sh -o wifi_diagnostic.sh && clear && sudo bash wifi_diagnostic.sh +``` + +→ Choose Option 3 → Option 5 (thermal issues) +Tool provides ASPM fixes and power management optimization + +## 📊 Example Analysis Output + +``` +🧠 === ENHANCED SYSTEM INTELLIGENCE GATHERING === +🐧 Distribution: Fedora Linux 39 +🔍 WiFi Interface: wlp1s0 +🔧 WiFi Functional Status: ✅ WORKING +🔧 Hardware: MediaTek Inc. MT7925 WiFi 7 (802.11be) +📊 Chip Analysis: + Vendor: MediaTek + Model: MT7925 + Generation: WiFi 7 (802.11be) - 160MHz capable + Known Issues: Excellent Linux support in kernel 6.8+ + +📡 === DFS CHANNEL ANALYSIS === +📍 Regulatory Domain: US +🔍 Current Connection DFS Analysis: + ⚠️ Currently connected to DFS channel: 100 (5500 MHz) + 🎯 DFS Impact: Medium to High risk of disconnections +🚨 Found 3 DFS/radar events in last 24 hours + +🎯 === FINAL ANALYSIS SUMMARY === +📊 Overall WiFi Health: Fair (DFS risk) (50/100) +🔧 WiFi Status: ✅ Working +📡 DFS Status: ⚠️ Connected to DFS channel (radar risk) +🌟 WiFi 7 Features: Standard WiFi 7 + +⚠️ DFS RECOMMENDATION: Switch to non-DFS channel for stability +💡 Suggested channels: 36, 40, 44, 48 (low 5GHz) or 149+ (high 5GHz) +``` + +## 🚀 Advanced Features + +### **Advanced Features** +- **Automatic Radar Detection**: Monitors system logs for DFS events +- **Environmental Mapping**: Scans for DFS channel usage in your area +- **Smart Channel Recommendations**: Suggests optimal non-DFS alternatives +- **6GHz Migration Planning**: Path to DFS-free operation + +### **Modern VPN Support** +- **Mesh VPN Optimization**: Tailscale, ZeroTier, Nebula configuration +- **Commercial VPN Fixes**: NordVPN, ExpressVPN, Surfshark compatibility +- **Split Tunneling**: Optimizes traffic routing for better performance +- **DNS Conflict Resolution**: Fixes modern VPN DNS issues + +### **WiFi 7 Optimization** +- **6GHz Band Access**: Identifies clean spectrum opportunities +- **320MHz Channels**: Ultra-wide channel detection and recommendations +- **MLO Support**: Multi-Link Operation analysis for maximum throughput +- **Advanced Power Management**: Thermal optimization for high-performance cards + +### **Distribution Intelligence** +- **Immutable Systems**: Special handling for Silverblue, Kinoite, Bluefin +- **Package Manager Detection**: Uses correct commands for dnf, apt, pacman, zypper +- **Firmware Management**: Distribution-specific update procedures +- **Kernel Parameter Handling**: Proper GRUB vs rpm-ostree vs bootc configuration + +## 💡 Pro Tips + +### **For System Administrators** +- Use Option 1 for baseline health assessment of fleet WiFi systems +- Option 4 provides regulatory compliance checking for enterprise environments +- Log outputs to files for trend analysis: `./wifi_diagnostic.sh | tee wifi_analysis.log` + +### **For Developers/Power Users** +- Option 6 provides direct CLI commands for automation and scripting +- All temporary fixes can be converted to permanent configurations +- Regulatory domain optimization maximizes performance within legal limits + +### **For Gamers/Streamers** +- DFS monitoring eliminates lagspikes from radar interference +- 6GHz optimization provides lowest latency connections +- Thermal management prevents throttling during extended use + +## 🔍 Troubleshooting Matrix + +| Symptom | Likely Cause | Tool Solution | +|---------|--------------|---------------| +| **Random 30s+ disconnections** | DFS radar interference | Option 4 → Non-DFS channels | +| **WiFi fails after suspend** | Driver power management | Option 3 → Suspend/resume fixes | +| **Slow WiFi 7 speeds** | Wrong band/channel width | Option 1 → 6GHz optimization | +| **VPN breaks WiFi** | MTU/routing conflicts | Option 3 → VPN optimization | +| **Overheating during WiFi** | ASPM/power management | Option 3 → Thermal fixes | +| **Connection fails entirely** | Driver/firmware issues | Option 2 → Distribution fixes | + +## 🛡️ Safety & Compatibility + +### **Safe Operation** +- All temporary changes revert on reboot +- Permanent changes clearly marked and reversible +- No destructive operations without explicit user confirmation +- Comprehensive logging for audit trails + +### **Broad Compatibility** +- **Distributions**: Fedora, Ubuntu, Debian, Arch (NetworkManager only), openSUSE, Pop!_OS, Mint, immutable variants +- **Hardware**: Intel, MediaTek, Qualcomm, Broadcom WiFi chipsets +- **Standards**: WiFi 4/5/6/6E/7, 2.4/5/6GHz bands +- **VPNs**: WireGuard, OpenVPN, modern mesh protocols +- **Network Managers**: NetworkManager (iwd support not yet implemented) + +## 📈 Future Development + +- [ ] iwd network manager support (currently NetworkManager only) +- [ ] Integration with WiFi 8 (802.11bn) when available +- [ ] Bluetooth coexistence analysis +- [ ] Automated performance benchmarking +- [ ] Web interface for remote diagnostics +- [ ] Integration with network monitoring systems + +## 🔗 Related Tools + +### **Specialized WiFi Analysis** +- **[Framework WiFi Mesh Network Analyzer](https://github.com/FrameworkComputer/linux-docs/tree/main/MeshAnalyzer#wifi-mesh-network-analyzer)** - Framework's dedicated tool for mesh network performance analysis and optimization + +### **When to Use Which Tool** +| Scenario | Enhanced WiFi Analyzer | Framework Mesh Analyzer | +|----------|----------------------|------------------------| +| **General WiFi issues** | ✅ Primary tool | ⚪ Not needed | +| **DFS disconnections** | ✅ Specialized detection | ⚪ Limited coverage | +| **VPN conflicts** | ✅ Modern VPN support | ⚪ Not covered | +| **Framework + Mesh** | ✅ General analysis | ✅ Mesh optimization | +| **Thermal/power issues** | ✅ Comprehensive | ⚪ Not covered | +| **Mesh performance tuning** | ⚪ Basic detection | ✅ Specialized analysis | + +### **Complementary Workflow** +1. **Start here** - Run Enhanced WiFi Analyzer for comprehensive system health +2. **Framework + Mesh users** - Follow up with Framework's Mesh Analyzer for specialized optimization +3. **Best results** - Use both tools for complete coverage of modern WiFi challenges + +## 🤝 Contributing + +Contributions welcome! Areas of particular interest: +- Additional chipset quirks and optimizations +- Distribution-specific command improvements +- VPN protocol support expansion +- Regional regulatory domain data +- Performance optimization techniques + +--- diff --git a/Enhanced-WiFi-Analyzer/scripts/readme b/Enhanced-WiFi-Analyzer/scripts/readme new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/Enhanced-WiFi-Analyzer/scripts/readme @@ -0,0 +1 @@ + diff --git a/Enhanced-WiFi-Analyzer/scripts/wifi_diagnostic.sh b/Enhanced-WiFi-Analyzer/scripts/wifi_diagnostic.sh new file mode 100644 index 0000000..2a40563 --- /dev/null +++ b/Enhanced-WiFi-Analyzer/scripts/wifi_diagnostic.sh @@ -0,0 +1,3649 @@ +# Look for DFS-related events in journalctl#!/bin/bash + +# Advanced WiFi Disconnect Intelligence Analyzer +# Enhanced with VPN detection, RF/frequency analysis, WiFi 7 support, distribution detection, and DFS monitoring +# Corrected version + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +BOLD='\033[1m' +CYAN='\033[0;36m' +MAGENTA='\033[0;35m' +NC='\033[0m' + +# Global variables +IFACE="" +WIFI_HARDWARE="" +DRIVER="" +CURRENT_FREQ="" +CURRENT_BAND="" +CURRENT_SSID="" +POWER_SAVE="" +INTELLIGENCE_SCORE=0 +DISCONNECT_PATTERN="" +VPN_ACTIVE="No" +VPN_TYPE="None" +VPN_INTERFACE="None" +VPN_IMPACT_SCORE=0 +WIFI_FUNCTIONAL=false +SEVERE_ISSUES=0 +FAILURE_LOG_DIR="/tmp/wifi_failure_logs" +DISTRO_ID="" +DISTRO_NAME="" +SUPPORTS_WIFI7=false +SUPPORTS_MLO=false +CHANNEL_WIDTH="" +CHIP_MODEL="" +CHIP_VENDOR="" +CHIP_GENERATION="" +KNOWN_ISSUES="" +DRIVER_NAME="" +CURRENT_SIGNAL="" +CURRENT_BITRATE="" +SCAN_RESULTS="" + +# Function to sanitize numeric variables +sanitize_number() { + local value="$1" + local default="${2:-0}" + + # Extract first line, remove non-digits, default to 0 if empty + echo "$value" | head -1 | tr -d '\n' | grep -o '[0-9]*' | head -1 | sed 's/^$/'"$default"'/' +} + +# DFS-specific global variables +DFS_CHANNELS_DETECTED=0 +DFS_CURRENT_CONNECTION=false +DFS_RADAR_EVENTS=0 +DFS_CHANNELS_LIST="" +DFS_IMPACT_SCORE=0 +DFS_CAC_EVENTS=0 +DFS_COUNT=0 + +# DFS channel definitions by region (focusing on common regions) +declare -A DFS_CHANNELS +DFS_CHANNELS[US]="52 56 60 64 100 104 108 112 116 120 124 128 132 136 140 144" +DFS_CHANNELS[EU]="52 56 60 64 100 104 108 112 116 120 124 128 132 136 140" +DFS_CHANNELS[JP]="52 56 60 64 100 104 108 112 116 120 124 128 132 136 140" + +# Function to convert frequency to channel number +freq_to_channel() { + local freq=$1 + local channel="" + + # Convert floating point frequency to integer + local freq_int=$(printf "%.0f" "$freq" 2>/dev/null || echo "$freq" | cut -d'.' -f1) + + if [ "$freq_int" -ge 2412 ] && [ "$freq_int" -le 2484 ]; then + # 2.4GHz band + if [ "$freq_int" -eq 2484 ]; then + channel=14 + else + channel=$(echo "scale=0; ($freq_int - 2412) / 5 + 1" | bc 2>/dev/null || echo "$(( (freq_int - 2412) / 5 + 1 ))") + fi + elif [ "$freq_int" -ge 5000 ] && [ "$freq_int" -le 6000 ]; then + # 5GHz band + channel=$(echo "scale=0; ($freq_int - 5000) / 5" | bc 2>/dev/null || echo "$(( (freq_int - 5000) / 5 ))") + elif [ "$freq_int" -ge 6000 ] && [ "$freq_int" -le 7200 ]; then + # 6GHz band (simplified - actual 6GHz channels are more complex) + channel="6GHz-$(echo "scale=0; ($freq_int - 5950) / 5" | bc 2>/dev/null || echo "$(( (freq_int - 5950) / 5 ))")" + fi + + echo "$channel" +} + +# Check if channel is DFS +is_dfs_channel() { + local channel=$1 + local region=${2:-US} # Default to US + + # Skip 6GHz channels (no DFS in 6GHz) + if echo "$channel" | grep -q "6GHz"; then + return 1 + fi + + # Get DFS channels for region + local dfs_list="${DFS_CHANNELS[$region]}" + + # Check if channel is in DFS list + for dfs_ch in $dfs_list; do + if [ "$channel" = "$dfs_ch" ]; then + return 0 # Is DFS + fi + done + + return 1 # Not DFS +} + +# Enhanced DFS channel analysis +analyze_dfs_channels() { + echo -e "${MAGENTA}📡 === DFS CHANNEL ANALYSIS ===${NC}" + echo "🔍 Dynamic Frequency Selection monitoring for radar interference" + echo "" + + if [ -z "$IFACE" ]; then + echo "❌ Cannot analyze DFS channels - WiFi interface not available" + return 1 + fi + + # Get regulatory domain + REG_DOMAIN="US" # Default + REG_INFO=$(iw reg get 2>/dev/null) + if [ -n "$REG_INFO" ]; then + REG_COUNTRY=$(echo "$REG_INFO" | grep "country" | head -1 | awk '{print $2}' | tr -d ':') + if [ -n "$REG_COUNTRY" ]; then + REG_DOMAIN="$REG_COUNTRY" + fi + fi + + echo "📍 Regulatory Domain: $REG_DOMAIN" + echo "📋 DFS Channels in $REG_DOMAIN: ${DFS_CHANNELS[$REG_DOMAIN]:-${DFS_CHANNELS[US]}}" + echo "" + + # Get current connection details if not already available +if [ -z "$CURRENT_FREQ" ]; then + WIFI_INFO=$(iw dev "$IFACE" link 2>/dev/null) + if ! echo "$WIFI_INFO" | grep -q "Not connected"; then + CURRENT_FREQ=$(echo "$WIFI_INFO" | grep "freq:" | awk '{print $2}') + CURRENT_SSID=$(echo "$WIFI_INFO" | grep "SSID:" | awk '{print $2}') + fi +fi + +# Check current connection for DFS usage +echo "🔍 Current Connection DFS Analysis:" +if [ -n "$CURRENT_FREQ" ] && [ "$CURRENT_FREQ" != "Unknown" ]; then + CURRENT_CHANNEL=$(freq_to_channel "$CURRENT_FREQ") + + if is_dfs_channel "$CURRENT_CHANNEL" "$REG_DOMAIN"; then + DFS_CURRENT_CONNECTION=true + echo -e " ${YELLOW}⚠️ Currently connected to DFS channel: $CURRENT_CHANNEL ($CURRENT_FREQ MHz)${NC}" + echo " 🎯 DFS Impact: Medium to High risk of disconnections" + DFS_IMPACT_SCORE=75 + else + DFS_CURRENT_CONNECTION=false + echo -e " ${GREEN}✅ Current channel $CURRENT_CHANNEL ($CURRENT_FREQ MHz) is NOT DFS${NC}" + echo " 🎯 DFS Impact: No risk from current connection" + DFS_IMPACT_SCORE=0 + fi +else + echo " ❓ Cannot determine current channel - not connected" +fi +echo "" + + # Scan for DFS channels in environment + echo "🔍 Scanning for DFS channels in area..." + + SCAN_RESULTS=$(timeout 20 iw dev "$IFACE" scan 2>/dev/null) + + if [ -n "$SCAN_RESULTS" ]; then + DFS_CHANNELS_DETECTED=0 + DFS_NETWORKS_LIST="" + + # Parse scan results for DFS channels + echo "$SCAN_RESULTS" | grep -E "freq:|SSID:" | while IFS= read -r line; do + if echo "$line" | grep -q "freq:"; then + FREQ=$(echo "$line" | awk '{print $2}') + CHANNEL=$(freq_to_channel "$FREQ") + + if is_dfs_channel "$CHANNEL" "$REG_DOMAIN"; then + DFS_CHANNELS_DETECTED=$((DFS_CHANNELS_DETECTED + 1)) + # Get the SSID for this frequency (next SSID line after freq) + SSID=$(echo "$SCAN_RESULTS" | grep -A5 "freq: $FREQ" | grep "SSID:" | head -1 | awk '{print $2}') + if [ -n "$SSID" ] && [ "$SSID" != "\\x00" ]; then + echo " 🚨 DFS Network: $SSID (Channel $CHANNEL, $FREQ MHz)" + else + echo " 🚨 DFS Channel: $CHANNEL ($FREQ MHz) - Hidden SSID" + fi + fi + fi + done + + # Count DFS networks separately - FIXED VERSION + DFS_NETWORKS=$(echo "$SCAN_RESULTS" | awk ' + /freq:/ { + freq = $2; + # Convert floating point to integer for channel calculation + freq_int = int(freq + 0.5) + if (freq_int >= 5000 && freq_int <= 6000) { + channel = int((freq_int - 5000) / 5) + } else { + channel = 0 + } + } + /SSID:/ && $2 != "\\x00" && $2 != "" { + if (channel == 52 || channel == 56 || channel == 60 || channel == 64 || + (channel >= 100 && channel <= 144)) { + print $2 " (Ch " channel ", " freq " MHz)" + } + } + ') + + # FIXED: Get proper count without newlines + if [ -n "$DFS_NETWORKS" ]; then + DFS_COUNT=$(echo "$DFS_NETWORKS" | grep -c "Ch " 2>/dev/null) + else + DFS_COUNT=0 + fi + + # Ensure DFS_COUNT is a single integer + DFS_COUNT=$(sanitize_number "$DFS_COUNT" "0") + + echo "" + echo "📊 DFS Environment Summary:" + echo " DFS Networks Detected: $DFS_COUNT" + + if [ "$DFS_COUNT" -gt 0 ]; then + echo " 🚨 DFS Networks in Area:" + echo "$DFS_NETWORKS" | head -10 | while IFS= read -r network; do + if [ -n "$network" ]; then + echo " • $network" + fi + done + + if [ "$DFS_COUNT" -gt 10 ]; then + echo " ... and $((DFS_COUNT - 10)) more DFS networks" + fi + fi + else + echo " ❌ Cannot scan environment - scan failed" + DFS_COUNT=0 + fi + + echo "" + + # Check for recent DFS/radar events in system logs + echo "🔍 Checking for recent DFS/radar events..." + + DFS_RADAR_EVENTS=0 + DFS_CAC_EVENTS=0 + + # Look for radar detection events +RADAR_EVENTS=$(journalctl --since "24 hours ago" --no-pager 2>/dev/null | \ + grep -iE "radar.*(detect|found)|dfs.*(detect|switch|cac)|channel.*(blocked|switch).*radar|cfg80211.*radar|ieee80211.*radar|ath.*radar|iwlwifi.*radar|mt76.*radar" | \ + grep -v "packagekit\|cache\|python" | \ + wc -l) + + if [ "$RADAR_EVENTS" -gt 0 ]; then + DFS_RADAR_EVENTS="$RADAR_EVENTS" + echo -e " ${RED}🚨 Found $RADAR_EVENTS DFS/radar events in last 24 hours${NC}" + echo " 📋 Recent DFS events:" + journalctl --since "6 hours ago" --no-pager 2>/dev/null | \ + grep -iE "radar.*(detect|found)|dfs.*(detect|switch|cac)|channel.*(blocked|switch).*radar|cfg80211.*radar|ieee80211.*radar|ath.*radar|iwlwifi.*radar|mt76.*radar" | \ + grep -v "packagekit\|cache\|python" | \ + tail -5 | while IFS= read -r event; do + echo " $event" +done + + DFS_IMPACT_SCORE=$((DFS_IMPACT_SCORE + 50)) + else + echo -e " ${GREEN}✅ No recent DFS/radar events detected${NC}" + fi + + # Look for CAC (Channel Availability Check) events + if command -v dmesg >/dev/null 2>&1; then + CAC_EVENTS=$(dmesg | grep -iE "cac.*start|cac.*complete|cac.*abort" | wc -l) + if [ "$CAC_EVENTS" -gt 0 ]; then + DFS_CAC_EVENTS="$CAC_EVENTS" + echo " 📡 CAC (Channel Availability Check) events: $CAC_EVENTS" + echo " 💡 CAC events indicate DFS channel switching activity" + fi + fi + + echo "" + + # DFS Impact Assessment + echo "🎯 === DFS IMPACT ASSESSMENT ===" + + # Sanitize all numeric variables before comparisons + DFS_COUNT=$(sanitize_number "$DFS_COUNT" "0") + DFS_RADAR_EVENTS=$(sanitize_number "$DFS_RADAR_EVENTS" "0") + DFS_IMPACT_SCORE=$(sanitize_number "$DFS_IMPACT_SCORE" "0") + + if [ "$DFS_CURRENT_CONNECTION" = true ]; then + echo -e " ${YELLOW}⚠️ HIGH RISK: Connected to DFS channel${NC}" + echo " 💡 DFS channels must stop transmission when radar is detected" + echo " 💡 This can cause sudden disconnections lasting 30+ seconds" + fi + + if [ "$DFS_COUNT" -gt 5 ]; then + echo -e " ${YELLOW}⚠️ MEDIUM RISK: High DFS usage in area ($DFS_COUNT networks)${NC}" + echo " 💡 Heavy DFS usage indicates radar-prone environment" + DFS_IMPACT_SCORE=$((DFS_IMPACT_SCORE + 25)) + elif [ "$DFS_COUNT" -gt 0 ]; then + echo -e " ${GREEN}✅ LOW RISK: Some DFS usage in area ($DFS_COUNT networks)${NC}" + echo " 💡 Moderate DFS environment - watch for patterns" + DFS_IMPACT_SCORE=$((DFS_IMPACT_SCORE + 10)) + else + echo -e " ${GREEN}✅ NO RISK: No DFS channels detected in area${NC}" + echo " 💡 Clean environment - DFS not a factor" + fi + + if [ "$DFS_RADAR_EVENTS" -gt 0 ]; then + echo -e " ${RED}🚨 CRITICAL: Recent radar detection events${NC}" + echo " 💡 Active radar environment - expect frequent DFS disconnections" + fi + + echo "" + echo "📊 DFS Risk Score: $DFS_IMPACT_SCORE/100" + + if [ "$DFS_IMPACT_SCORE" -ge 75 ]; then + echo -e " ${RED}🚨 HIGH DFS RISK - Immediate action recommended${NC}" + elif [ "$DFS_IMPACT_SCORE" -ge 50 ]; then + echo -e " ${YELLOW}⚠️ MODERATE DFS RISK - Monitor and optimize${NC}" + elif [ "$DFS_IMPACT_SCORE" -ge 25 ]; then + echo -e " ${YELLOW}📊 LOW DFS RISK - Minor impact possible${NC}" + else + echo -e " ${GREEN}✅ MINIMAL DFS RISK - Not a significant factor${NC}" + fi + + echo "" +} + +# Test actual connectivity with VPN awareness +test_actual_connectivity() { + echo "🔍 Testing WiFi connection and data flow..." + + if [ -n "$IFACE" ]; then + WIFI_LINK_STATUS=$(iw dev "$IFACE" link 2>/dev/null) + + if echo "$WIFI_LINK_STATUS" | grep -q "Not connected"; then + echo " ❌ SEVERE: WiFi not connected to any network" + return 1 + elif echo "$WIFI_LINK_STATUS" | grep -q "Connected to"; then + CURRENT_SSID=$(echo "$WIFI_LINK_STATUS" | grep "SSID" | awk '{print $2}') + echo " 📡 WiFi connected to: ${CURRENT_SSID:-Unknown SSID}" + + # Test internet connectivity with VPN awareness + if ! timeout 10 ping -c 3 -W 5 8.8.8.8 >/dev/null 2>&1; then + echo " ❌ SEVERE: No internet connectivity despite WiFi connection" + if [ "$VPN_ACTIVE" = "Yes" ]; then + echo " 🔒 VPN active - may be blocking or routing traffic incorrectly" + fi + return 1 + fi + + echo " ✅ Data flow verified - connectivity working" + return 0 + fi + fi + + return 1 +} + +# Severe issue detection with DFS awareness +detect_severe_wifi_issues() { + echo -e "${RED}🚨 === SEVERE ISSUE DETECTION ===${NC}" + echo "🔍 Testing for issues requiring NetworkManager restart..." + echo "" + + SEVERE_ISSUES=0 + + # Test actual connectivity + if ! test_actual_connectivity; then + SEVERE_ISSUES=$((SEVERE_ISSUES + 1)) + fi + + # Check for DFS-related disconnection patterns + echo "" + echo "🔍 Checking for DFS-related disconnection patterns..." + + if [ "$DFS_CURRENT_CONNECTION" = true ] && [ "$DFS_RADAR_EVENTS" -gt 0 ]; then + echo -e "${RED}🚨 DFS RADAR INTERFERENCE DETECTED${NC}" + echo " Current connection uses DFS channel with recent radar events" + echo " This likely explains WiFi disconnections" + SEVERE_ISSUES=$((SEVERE_ISSUES + 1)) + elif [ "$DFS_CURRENT_CONNECTION" = true ]; then + echo -e "${YELLOW}⚠️ DFS RISK: Connected to radar-sensitive channel${NC}" + echo " Monitor for sudden disconnections lasting 30+ seconds" + fi + + echo "" + + # Provide distribution-aware recommendations + if [ "$SEVERE_ISSUES" -gt 0 ]; then + echo -e "${RED}🚨 SEVERE ISSUES DETECTED: $SEVERE_ISSUES${NC}" + echo "" + echo -e "${YELLOW}🔧 IMMEDIATE ACTION REQUIRED:${NC}" + echo "" + + if [ "$VPN_ACTIVE" = "Yes" ]; then + echo -e "${CYAN}🔒 VPN DETECTED - Try VPN-specific fixes first:${NC}" + echo "0. Disconnect VPN temporarily and test WiFi" + echo " If WiFi works without VPN, the issue is VPN-related" + echo "" + fi + + if [ "$DFS_CURRENT_CONNECTION" = true ]; then + echo -e "${MAGENTA}📡 DFS CHANNEL DETECTED - Try DFS-specific fixes first:${NC}" + echo "0a. Switch to non-DFS channel immediately:" + if [ -n "$CURRENT_SSID" ]; then + echo " sudo nmcli connection modify \"$CURRENT_SSID\" 802-11-wireless.band bg # Force 2.4GHz (no DFS)" + echo " sudo nmcli connection up \"$CURRENT_SSID\"" + fi + echo "0b. Configure router to use non-DFS channels: 36, 40, 44, 48 (low 5GHz)" + echo "0c. Alternative channels: 149, 153, 157, 161, 165 (high 5GHz)" + echo "" + fi + + echo "Standard WiFi fixes:" + echo "1. sudo systemctl restart NetworkManager" + echo "2. sudo modprobe -r $DRIVER && sleep 2 && sudo modprobe $DRIVER" + echo "3. sudo systemctl restart wpa_supplicant" + + if [ "$VPN_ACTIVE" = "Yes" ]; then + echo "" + echo "VPN-specific fixes:" + echo "4. Restart VPN service" + echo "5. Try different VPN server" + echo "6. Lower VPN MTU: sudo ip link set $VPN_INTERFACE mtu 1200" + fi + + else + echo -e "${GREEN}✅ NO SEVERE ISSUES DETECTED${NC}" + echo "" + echo "🎉 WiFi system is stable and functioning properly" + + # Still provide DFS recommendations if relevant + if [ "$DFS_IMPACT_SCORE" -gt 25 ]; then + echo "" + echo "💡 Note: DFS channels detected in environment - monitor for patterns" + fi + fi +} + +# Distribution detection +detect_distribution() { + if [ -f /etc/os-release ]; then + . /etc/os-release + DISTRO_ID="$ID" + DISTRO_NAME="$PRETTY_NAME" + + # Handle Bluefin/Silverblue/Kinoite as Fedora-based + if echo "$PRETTY_NAME" | grep -qi "bluefin\|silverblue\|kinoite"; then + DISTRO_ID="fedora" + echo "🔍 Detected immutable Fedora variant: $PRETTY_NAME" + elif echo "$ID_LIKE" | grep -qi "fedora"; then + DISTRO_ID="fedora" + elif echo "$ID_LIKE" | grep -qi "debian"; then + DISTRO_ID="debian" + elif echo "$ID_LIKE" | grep -qi "arch"; then + DISTRO_ID="arch" + fi + else + DISTRO_ID="unknown" + DISTRO_NAME="Unknown Linux" + fi +} + +# Get distribution-specific commands +get_distro_command() { + local command_type="$1" + + case "$DISTRO_ID" in + "fedora"|"rhel"|"centos"|"rocky"|"almalinux") + case "$command_type" in + "firmware_update") + # Check for bootc first (newer immutable systems like Bluefin) + if command -v bootc >/dev/null 2>&1; then + echo "sudo bootc upgrade || (rpm-ostree reset && sudo bootc upgrade)" + # Check for rpm-ostree (Silverblue/Kinoite) + elif command -v rpm-ostree >/dev/null 2>&1; then + echo "rpm-ostree update && sudo reboot" + else + echo "sudo dnf update linux-firmware" + fi + ;; + "grub_update") + # Immutable systems don't need manual GRUB updates + if command -v rpm-ostree >/dev/null 2>&1 || command -v bootc >/dev/null 2>&1; then + echo "# GRUB updated automatically on reboot for immutable systems" + else + echo "sudo grub2-mkconfig -o /boot/grub2/grub.cfg" + fi + ;; + "initrd_update") + # Immutable systems rebuild initrd automatically + if command -v rpm-ostree >/dev/null 2>&1 || command -v bootc >/dev/null 2>&1; then + echo "# initrd rebuilt automatically on reboot for immutable systems" + else + echo "sudo dracut -f" + fi + ;; + "kernel_param") + # Check for immutable Fedora variants first + if command -v rpm-ostree >/dev/null 2>&1; then + echo "sudo rpm-ostree kargs --append=" + elif command -v bootc >/dev/null 2>&1; then + echo "sudo bootc kargs --append=" + else + echo "sudo grubby --update-kernel=ALL --args=" + fi + ;; + "package_manager") + if command -v bootc >/dev/null 2>&1; then + echo "bootc" + elif command -v rpm-ostree >/dev/null 2>&1; then + echo "rpm-ostree" + else + echo "dnf" + fi + ;; + esac + ;; + "ubuntu"|"debian"|"pop"|"mint"|"linuxmint") + case "$command_type" in + "firmware_update") echo "sudo apt update && sudo apt upgrade linux-firmware" ;; + "grub_update") echo "sudo update-grub" ;; + "initrd_update") echo "sudo update-initramfs -u" ;; + "kernel_param") echo "Edit /etc/default/grub and add to GRUB_CMDLINE_LINUX=" ;; + "package_manager") echo "apt" ;; + esac + ;; + "arch"|"manjaro"|"endeavouros") + case "$command_type" in + "firmware_update") echo "sudo pacman -S linux-firmware" ;; + "grub_update") echo "sudo grub-mkconfig -o /boot/grub/grub.cfg" ;; + "initrd_update") echo "sudo mkinitcpio -P" ;; + "kernel_param") echo "Edit GRUB_CMDLINE_LINUX in /etc/default/grub" ;; + "package_manager") echo "pacman" ;; + esac + ;; + "opensuse"|"opensuse-leap"|"opensuse-tumbleweed") + case "$command_type" in + "firmware_update") echo "sudo zypper update kernel-firmware" ;; + "grub_update") echo "sudo grub2-mkconfig -o /boot/grub2/grub.cfg" ;; + "initrd_update") echo "sudo dracut -f" ;; + "kernel_param") echo "Edit /etc/default/grub and add to GRUB_CMDLINE_LINUX=" ;; + "package_manager") echo "zypper" ;; + esac + ;; + *) + case "$command_type" in + "firmware_update") echo "# Update firmware using your distribution's package manager" ;; + "grub_update") echo "# Update GRUB configuration" ;; + "initrd_update") echo "# Rebuild initramfs" ;; + "kernel_param") echo "# Edit GRUB configuration manually" ;; + "package_manager") echo "# Use your distribution's package manager" ;; + esac + ;; + esac +} + +# Function to run command with privilege +run_with_privilege() { + if [ "$EUID" -eq 0 ]; then + "$@" + else + if sudo -n true 2>/dev/null; then + sudo "$@" + else + "$@" 2>/dev/null + fi + fi +} + +# Test actual WiFi functionality - THE SINGLE SOURCE OF TRUTH +test_wifi_functionality() { + local functional=false + + if [ -n "$IFACE" ]; then + # Test 1: Interface exists and is up + if ip link show "$IFACE" 2>/dev/null | grep -q "state UP"; then + # Test 2: Check actual WiFi connection status first + WIFI_LINK_STATUS=$(iw dev "$IFACE" link 2>/dev/null) + + if echo "$WIFI_LINK_STATUS" | grep -q "Connected to"; then + # Test 3: Driver is responding to commands + if iw dev "$IFACE" info >/dev/null 2>&1; then + # Test 4: Internet connectivity working + if ping -c 1 -W 2 1.1.1.1 >/dev/null 2>&1; then + functional=true + fi + fi + fi + fi + fi + + echo "$functional" +} + +# Modern chipset analysis - ENHANCED with PCI ID detection +analyze_modern_chipsets() { + # Method 1: Check by chip name in hardware string + if echo "$WIFI_HARDWARE" | grep -qi "mt7925"; then + CHIP_MODEL="MT7925" + CHIP_GENERATION="WiFi 7 (802.11be) - 160MHz capable" + KNOWN_ISSUES="Excellent Linux support in kernel 6.8+, stable WiFi 7 implementation" + DRIVER_NAME="mt7925e" + SUPPORTS_WIFI7=true + elif echo "$WIFI_HARDWARE" | grep -qi "mt7927"; then + CHIP_MODEL="MT7927" + CHIP_GENERATION="WiFi 7 (802.11be) - 320MHz capable" + KNOWN_ISSUES="Advanced WiFi 7 with 320MHz, requires kernel 6.9+" + DRIVER_NAME="mt7925e" + SUPPORTS_WIFI7=true + # Method 2: Check by PCI Device ID (for newer chips not yet named in lspci database) + elif echo "$WIFI_HARDWARE" | grep -qi "device 0717"; then + CHIP_MODEL="MT7925" + CHIP_GENERATION="WiFi 7 (802.11be) - 6GHz + MLO capable" + KNOWN_ISSUES="Latest WiFi 7 with full 6GHz and MLO support, requires kernel 6.8+" + DRIVER_NAME="mt7925e" + SUPPORTS_WIFI7=true + SUPPORTS_MLO=true + elif echo "$WIFI_HARDWARE" | grep -qi "device 0718\|device 0719"; then + CHIP_MODEL="MT7925/MT7927 variant" + CHIP_GENERATION="WiFi 7 (802.11be) - Advanced features" + KNOWN_ISSUES="Cutting-edge WiFi 7, may need latest firmware" + DRIVER_NAME="mt7925e" + SUPPORTS_WIFI7=true + SUPPORTS_MLO=true + elif echo "$WIFI_HARDWARE" | grep -qi "qcncm865\|wcn7850"; then + CHIP_MODEL="Qualcomm WCN7850/FastConnect 7800" + CHIP_GENERATION="WiFi 7 (802.11be) with MLO" + KNOWN_ISSUES="Excellent WiFi 7 MLO support via ath12k driver" + DRIVER_NAME="ath12k" + SUPPORTS_WIFI7=true + SUPPORTS_MLO=true + elif echo "$WIFI_HARDWARE" | grep -qi "be200"; then + CHIP_MODEL="Intel BE200" + CHIP_GENERATION="WiFi 7 (802.11be)" + KNOWN_ISSUES="Intel WiFi 7, requires kernel 6.8+, AMD compatibility issues" + DRIVER_NAME="iwlwifi" + SUPPORTS_WIFI7=true + elif echo "$WIFI_HARDWARE" | grep -qi "mt7922"; then + CHIP_MODEL="MT7922" + CHIP_GENERATION="WiFi 6E (802.11ax) with WiFi 7 features" + KNOWN_ISSUES="Mature WiFi 6E with some WiFi 7 capabilities, excellent 6GHz support" + DRIVER_NAME="mt7921e" + # MT7922 has some WiFi 7 features even though it's primarily WiFi 6E + SUPPORTS_WIFI7=true + elif echo "$WIFI_HARDWARE" | grep -qi "ax210\|ax211"; then + CHIP_MODEL="Intel AX210/AX211" + CHIP_GENERATION="WiFi 6E (802.11ax)" + KNOWN_ISSUES="Stable WiFi 6E with 6GHz support" + DRIVER_NAME="iwlwifi" + fi + + # Set chip vendor properly + if echo "$WIFI_HARDWARE" | grep -qi "mediatek"; then + CHIP_VENDOR="MediaTek" + elif echo "$WIFI_HARDWARE" | grep -qi "intel"; then + CHIP_VENDOR="Intel" + elif echo "$WIFI_HARDWARE" | grep -qi "qualcomm\|qcom"; then + CHIP_VENDOR="Qualcomm" + else + CHIP_VENDOR="Unknown" + fi +} + +# Enhanced channel width detection for modern WiFi +detect_advanced_channel_width() { + # Multiple detection methods for WiFi 6E/7 + CHANNEL_WIDTH="" + + # Method 1: Direct iw link parsing + CHANNEL_WIDTH=$(iw dev "$IFACE" link 2>/dev/null | grep -oE "(20|40|80|160|320)MHz" | head -1 | grep -o "[0-9]*") + + if [ -z "$CHANNEL_WIDTH" ]; then + # Method 2: Parse from width field + CHANNEL_WIDTH=$(iw dev "$IFACE" link 2>/dev/null | grep -oE "width: [0-9]+" | awk '{print $2}') + fi + + if [ -z "$CHANNEL_WIDTH" ]; then + # Method 3: WiFi 7 detection - Look for BE-MCS indicators + BE_MCS=$(iw dev "$IFACE" link 2>/dev/null | grep "BE-MCS") + HE_MCS=$(iw dev "$IFACE" link 2>/dev/null | grep "HE-MCS") + + if [ -n "$BE_MCS" ] || [ -n "$HE_MCS" ]; then + # Estimate from bitrate (WiFi 6E/7 specific) + BITRATE_NUM=$(echo "$CURRENT_BITRATE" | grep -o "[0-9]*" | head -1) + if [ -n "$BITRATE_NUM" ]; then + if [ "$BITRATE_NUM" -gt 5000 ]; then + CHANNEL_WIDTH="320" + echo " 📊 Estimated channel width: 320MHz (based on $BITRATE_NUM Mbps - WiFi 7)" + elif [ "$BITRATE_NUM" -gt 2000 ]; then + CHANNEL_WIDTH="160" + echo " 📊 Estimated channel width: 160MHz (based on $BITRATE_NUM Mbps - WiFi 6E)" + elif [ "$BITRATE_NUM" -gt 1000 ]; then + CHANNEL_WIDTH="80" + echo " 📊 Estimated channel width: 80MHz (based on $BITRATE_NUM Mbps)" + fi + fi + fi + fi + + if [ -n "$CHANNEL_WIDTH" ] && [ "$CHANNEL_WIDTH" != "0" ]; then + echo " Current channel width: $CHANNEL_WIDTH MHz" + + # Modern WiFi analysis + case "$CHANNEL_WIDTH" in + "320") + echo " 🚀 Ultra-wide 320MHz - WiFi 7 maximum performance mode" + echo " 💡 Requires clean 6GHz spectrum and WiFi 7 router" + ;; + "160") + echo " 📊 Wide 160MHz - WiFi 6E/7 high performance mode" + echo " 💡 Excellent for high-bandwidth applications" + ;; + "80") + echo " 📈 Standard 80MHz - WiFi 6 optimal performance" + ;; + "40") + echo " 📊 Narrow 40MHz - conservative bandwidth" + ;; + "20") + echo " 📉 Basic 20MHz - maximum compatibility mode" + ;; + esac + else + echo " ⚠️ Channel width not detected - check modern WiFi support" + fi +} + +# Enhanced VPN Detection System with modern protocols +detect_vpn_configuration() { + echo -e "${CYAN}🔒 === VPN DETECTION & ANALYSIS ===${NC}" + echo "" + + local vpn_detected=false + local detected_vpn_type="None" + local detected_vpn_interface="None" + local vpn_impact_score=0 + + # Method 1: Enhanced VPN interface detection using more reliable parsing + echo "🔍 Scanning for VPN interfaces..." + + # Enhanced interface detection using reliable methods + VPN_IFACES=$(ip -o link show | awk -F': ' '{print $2}' | grep -Ei 'tailscale|zerotier|nebula|wg[0-9]*|tun[0-9]*|tap[0-9]*|nordlynx|utun[0-9]*') + + if [ -n "$VPN_IFACES" ]; then + while IFS= read -r vpn_iface; do + if [ -n "$vpn_iface" ]; then + # Check if interface is UP + if ip link show "$vpn_iface" 2>/dev/null | grep -q "state UP"; then + vpn_detected=true + + # Determine modern VPN type based on interface name + if echo "$vpn_iface" | grep -qi "tailscale"; then + detected_vpn_type="Tailscale (WireGuard-based mesh)" + elif echo "$vpn_iface" | grep -qi "zerotier"; then + detected_vpn_type="ZeroTier (SD-WAN)" + elif echo "$vpn_iface" | grep -qi "nebula"; then + detected_vpn_type="Nebula (Overlay mesh)" + elif echo "$vpn_iface" | grep -qi "wg"; then + detected_vpn_type="WireGuard" + elif echo "$vpn_iface" | grep -qi "tun"; then + detected_vpn_type="OpenVPN/Generic TUN" + elif echo "$vpn_iface" | grep -qi "nordlynx"; then + detected_vpn_type="NordVPN (WireGuard)" + else + detected_vpn_type="Unknown VPN" + fi + + echo " ✅ Active VPN: $vpn_iface ($detected_vpn_type)" + detected_vpn_interface="$vpn_iface" + break # Exit after finding first active VPN interface + fi + fi + done <<< "$VPN_IFACES" + fi + + # Method 2: Fallback - Check for VPN processes if no interface found + if [ "$vpn_detected" != "true" ]; then + echo "" + echo "🔍 Scanning for modern VPN processes..." + + VPN_PROCESSES=$(ps aux | grep -E "tailscaled|zerotier-one|nebula|headscale|wireguard|wg-quick|nordvpn|expressvpn|surfshark|mullvad|protonvpn" | grep -v grep) + + if [ -n "$VPN_PROCESSES" ]; then + vpn_detected=true + echo " 📋 Active modern VPN processes detected:" + while IFS= read -r process; do + if echo "$process" | grep -q "tailscaled"; then + echo " • tailscaled" + detected_vpn_type="Tailscale (WireGuard-based mesh)" + # Try to find Tailscale interface using more methods + if [ "$detected_vpn_interface" = "None" ]; then + TAILSCALE_IF=$(ip -o link show | awk -F': ' '{print $2}' | grep -i tailscale | head -1) + if [ -n "$TAILSCALE_IF" ]; then + detected_vpn_interface="$TAILSCALE_IF" + else + TAILSCALE_IF=$(ip addr show | grep -E "inet 100\." | awk '{print $NF}' | head -1) + detected_vpn_interface="${TAILSCALE_IF:-tailscale (process-based detection)}" + fi + fi + elif echo "$process" | grep -q "zerotier-one"; then + echo " • zerotier-one" + detected_vpn_type="ZeroTier (SD-WAN)" + elif echo "$process" | grep -q "nordvpn"; then + echo " • nordvpn" + detected_vpn_type="NordVPN" + else + PROC_NAME=$(echo "$process" | awk '{for(i=11;i<=NF;i++) printf "%s ", $i; print ""}' | sed 's/[[:space:]]*$//') + echo " • $PROC_NAME" + detected_vpn_type="Modern VPN" + fi + done <<< "$VPN_PROCESSES" + fi + fi + + # Method 3: Check for DNS changes (modern VPN DNS) + echo "" + echo "🔍 Checking DNS configuration for VPN indicators..." + + if command -v resolvectl >/dev/null 2>&1; then + DNS_INFO=$(resolvectl status 2>/dev/null) + COMMON_DNS=$(echo "$DNS_INFO" | grep -E "1\.1\.1\.1|8\.8\.8\.8|208\.67\.222\.222|94\.140\.14\.14") + + if [ -n "$COMMON_DNS" ]; then + echo " 📡 Public DNS detected (may be VPN-managed):" + echo "$COMMON_DNS" | head -3 | while IFS= read -r dns; do + echo " $dns" + done + fi + fi + + # VPN Impact Analysis + echo "" + echo "🎯 === VPN IMPACT ANALYSIS ===" + + # Set global variables based on detection + VPN_ACTIVE="No" + VPN_TYPE="$detected_vpn_type" + VPN_INTERFACE="$detected_vpn_interface" + VPN_IMPACT_SCORE="$vpn_impact_score" + + if [ "$vpn_detected" = true ]; then + VPN_ACTIVE="Yes" + echo -e " ${GREEN}✅ VPN Active: $detected_vpn_type${NC}" + echo " Interface: $detected_vpn_interface" + else + VPN_ACTIVE="No" + VPN_TYPE="None" + VPN_INTERFACE="None" + VPN_IMPACT_SCORE=0 + echo -e " ${GREEN}✅ No VPN detected${NC}" + echo " 💡 WiFi issues not related to VPN configuration" + fi + + echo "" +} + +# RF and Frequency Analysis System (enhanced for WiFi 7) - WITH DFS INTEGRATION +analyze_rf_frequency_environment() { + echo -e "${CYAN}📡 === RF & FREQUENCY ENVIRONMENT ANALYSIS ===${NC}" + echo "" + + if [ -z "$IFACE" ]; then + echo "❌ Cannot analyze RF environment - WiFi interface not available" + return 1 + fi + + # Get current connection details + echo "🔍 Current WiFi Connection Analysis..." + + WIFI_INFO=$(iw dev "$IFACE" link 2>/dev/null) + + if echo "$WIFI_INFO" | grep -q "Not connected"; then + echo " ❌ WiFi not connected - cannot analyze current RF environment" + return 1 + fi + + # Extract current RF parameters + CURRENT_FREQ=$(echo "$WIFI_INFO" | grep "freq:" | awk '{print $2}') + CURRENT_SIGNAL=$(echo "$WIFI_INFO" | grep "signal:" | awk '{print $2}') + CURRENT_BITRATE=$(echo "$WIFI_INFO" | grep "tx bitrate:" | awk '{print $3}') + CURRENT_SSID=$(echo "$WIFI_INFO" | grep "SSID:" | awk '{print $2}') + + echo "📊 Current RF Status:" + echo " SSID: ${CURRENT_SSID:-Unknown}" + echo " Frequency: ${CURRENT_FREQ:-Unknown} MHz" + echo " Signal: ${CURRENT_SIGNAL:-Unknown} dBm" + echo " TX Bitrate: ${CURRENT_BITRATE:-Unknown} Mbps" + + # Determine band and channel with modern WiFi support + if [ -n "$CURRENT_FREQ" ]; then + FREQ_INT=$(echo "$CURRENT_FREQ" | cut -d'.' -f1) + + if [ "$FREQ_INT" -ge 2400 ] && [ "$FREQ_INT" -le 2500 ]; then + CURRENT_BAND="2.4 GHz" + CHANNEL=$(echo "scale=0; ($CURRENT_FREQ - 2412) / 5 + 1" | bc 2>/dev/null || echo "Unknown") + elif [ "$FREQ_INT" -ge 5000 ] && [ "$FREQ_INT" -le 6000 ]; then + CURRENT_BAND="5 GHz" + CHANNEL=$(echo "scale=0; ($CURRENT_FREQ - 5000) / 5" | bc 2>/dev/null || echo "Unknown") + elif [ "$FREQ_INT" -ge 6000 ] && [ "$FREQ_INT" -le 7200 ]; then + CURRENT_BAND="6 GHz (WiFi 6E/7)" + CHANNEL="6GHz Channel" + else + CURRENT_BAND="Unknown" + CHANNEL="Unknown" + fi + + echo " Band: $CURRENT_BAND" + echo " Channel: $CHANNEL" + + # DFS Analysis for current connection + if [ "$CURRENT_BAND" = "5 GHz" ] && [ "$CHANNEL" != "Unknown" ]; then + if is_dfs_channel "$CHANNEL"; then + echo -e " ${YELLOW}⚠️ DFS Channel: Current connection uses DFS channel $CHANNEL${NC}" + echo " 💡 DFS channels can cause disconnections when radar is detected" + else + echo -e " ${GREEN}✅ Non-DFS Channel: Current channel $CHANNEL is safe from radar interference${NC}" + fi + fi + fi + + # Enhanced channel width detection + detect_advanced_channel_width + + # 6GHz environment analysis + analyze_6ghz_environment + + # RF Environment Scan + echo "" + echo "🔍 Scanning RF environment for interference..." + + SCAN_RESULTS=$(timeout 15 iw dev "$IFACE" scan 2>/dev/null) + + if [ -n "$SCAN_RESULTS" ]; then + # Count networks by band (including 6GHz) + NETWORKS_24=$(echo "$SCAN_RESULTS" | grep "freq:" | awk '{print $2}' | awk '$1 >= 2400 && $1 <= 2500' | wc -l) + NETWORKS_5=$(echo "$SCAN_RESULTS" | grep "freq:" | awk '{print $2}' | awk '$1 >= 5000 && $1 <= 6000' | wc -l) + NETWORKS_6=$(echo "$SCAN_RESULTS" | grep "freq:" | awk '{print $2}' | awk '$1 >= 6000 && $1 <= 7200' | sort -u | wc -l) + + echo "📊 Nearby Networks:" + echo " 2.4 GHz: $NETWORKS_24 networks" + echo " 5 GHz: $NETWORKS_5 networks" + echo " 6 GHz: $NETWORKS_6 networks" + + # Find strongest interfering networks + echo "" + echo "🚨 Top interfering networks on your band:" + + case "$CURRENT_BAND" in + "2.4 GHz") FREQ_RANGE="freq: 24[0-9][0-9]" ;; + "5 GHz") FREQ_RANGE="freq: 5[0-9][0-9][0-9]" ;; + "6 GHz (WiFi 6E/7)") FREQ_RANGE="freq: 6[0-9][0-9][0-9]" ;; + *) FREQ_RANGE="freq:" ;; + esac + + # Extract and sort networks by signal strength - FIXED signal filter + echo "$SCAN_RESULTS" | grep -A10 -B2 "$FREQ_RANGE" | grep -E "BSS|signal|SSID|freq:" | \ + awk '/BSS/ {bss=$2} /freq:/ {freq=$2} /signal:/ {signal=$2} /SSID:/ {ssid=$2; if(signal<-20 && ssid!="") print signal " dBm - " ssid " (" freq " MHz)"}' | \ + sort -n | tail -5 | while IFS= read -r network; do + echo " $network" + done + + else + echo " ❌ Cannot scan RF environment - scan failed" + fi + + # Power and regulatory analysis + echo "" + echo "🔍 Power and regulatory analysis..." + + REG_INFO=$(iw reg get 2>/dev/null) + if [ -n "$REG_INFO" ]; then + COUNTRY_LINE=$(echo "$REG_INFO" | grep "country" | head -1) + GLOBAL_LINE=$(echo "$REG_INFO" | grep "global") + + if [ -n "$COUNTRY_LINE" ]; then + REG_DOMAIN="$COUNTRY_LINE" + elif [ -n "$GLOBAL_LINE" ]; then + REG_DOMAIN="$GLOBAL_LINE" + else + REG_DOMAIN=$(echo "$REG_INFO" | head -1) + fi + else + REG_DOMAIN="Not available" + fi + echo " Regulatory domain: ${REG_DOMAIN:-Unknown}" + + TX_POWER=$(iw dev "$IFACE" info 2>/dev/null | grep "txpower" | awk '{print $2, $3}') + echo " TX Power: ${TX_POWER:-Unknown}" + + # RF Quality Assessment + echo "" + echo "🎯 === RF QUALITY ASSESSMENT ===" + + RF_SCORE=100 + RF_ISSUES=() + + # Signal strength assessment - ACTUALLY FIXED LOGIC + if [ -n "$CURRENT_SIGNAL" ]; then + # Extract just the number (keep it positive for easier comparison) + SIGNAL_NUM=$(echo "$CURRENT_SIGNAL" | sed 's/-//') + + if command -v bc >/dev/null 2>&1; then + # CORRECTLY FIXED: Lower absolute values = better signal strength + # Remember: -30 dBm is excellent, -90 dBm is terrible + if [ "$(echo "$SIGNAL_NUM <= 40" | bc)" -eq 1 ]; then + echo " ✅ Excellent signal strength ($CURRENT_SIGNAL dBm)" + # No penalty for excellent signal + elif [ "$(echo "$SIGNAL_NUM <= 60" | bc)" -eq 1 ]; then + echo " 📊 Good signal strength ($CURRENT_SIGNAL dBm)" + RF_SCORE=$((RF_SCORE - 10)) + elif [ "$(echo "$SIGNAL_NUM <= 80" | bc)" -eq 1 ]; then + echo " ⚠️ Weak signal strength ($CURRENT_SIGNAL dBm)" + RF_SCORE=$((RF_SCORE - 30)) + RF_ISSUES+=("Weak signal - move closer to router") + else + echo " 🚨 Poor signal strength ($CURRENT_SIGNAL dBm)" + RF_SCORE=$((RF_SCORE - 50)) + RF_ISSUES+=("Very poor signal - major connectivity issues expected") + fi + else + # Fallback without bc - CORRECTLY FIXED logic + SIGNAL_INT=$(printf "%.0f" "$SIGNAL_NUM" 2>/dev/null || echo "$SIGNAL_NUM") + if [ "$SIGNAL_INT" -le 40 ]; then + echo " ✅ Excellent signal strength ($CURRENT_SIGNAL dBm)" + # No penalty for excellent signal + elif [ "$SIGNAL_INT" -le 60 ]; then + echo " 📊 Good signal strength ($CURRENT_SIGNAL dBm)" + RF_SCORE=$((RF_SCORE - 10)) + elif [ "$SIGNAL_INT" -le 80 ]; then + echo " ⚠️ Weak signal strength ($CURRENT_SIGNAL dBm)" + RF_SCORE=$((RF_SCORE - 30)) + RF_ISSUES+=("Weak signal - move closer to router") + else + echo " 🚨 Poor signal strength ($CURRENT_SIGNAL dBm)" + RF_SCORE=$((RF_SCORE - 50)) + RF_ISSUES+=("Very poor signal - major connectivity issues expected") + fi + fi + fi + + # Modern congestion assessment + if [ "$CURRENT_BAND" = "2.4 GHz" ] && [ "$NETWORKS_24" -gt 15 ]; then + echo " 🚨 High 2.4GHz congestion ($NETWORKS_24 networks)" + RF_SCORE=$((RF_SCORE - 25)) + RF_ISSUES+=("Switch to 5GHz or 6GHz if available") + elif [ "$CURRENT_BAND" = "5 GHz" ] && [ "$NETWORKS_5" -gt 20 ]; then + echo " ⚠️ Moderate 5GHz congestion ($NETWORKS_5 networks)" + RF_SCORE=$((RF_SCORE - 15)) + RF_ISSUES+=("Consider WiFi 6E/7 (6GHz) if available") + elif [ "$CURRENT_BAND" = "6 GHz (WiFi 6E/7)" ]; then + echo " 🌟 6GHz band - clean spectrum advantage" + RF_SCORE=$((RF_SCORE + 10)) # Bonus for 6GHz + fi + + # Final RF assessment + echo "" + echo "📊 RF Environment Score: $RF_SCORE/100" + + if [ "$RF_SCORE" -ge 90 ]; then + echo -e " ${GREEN}🌟 Outstanding RF environment${NC}" + elif [ "$RF_SCORE" -ge 80 ]; then + echo -e " ${GREEN}✅ Excellent RF environment${NC}" + elif [ "$RF_SCORE" -ge 60 ]; then + echo -e " ${YELLOW}⚠️ Good RF environment with minor issues${NC}" + elif [ "$RF_SCORE" -ge 40 ]; then + echo -e " ${YELLOW}🚨 Poor RF environment - optimization needed${NC}" + else + echo -e " ${RED}🚨 Critical RF environment - major issues${NC}" + fi + + echo "" + + # INTEGRATED DFS ANALYSIS + analyze_dfs_channels +} + +# Fixed and Enhanced 6GHz analysis function - ENHANCED for MT7925 PCI ID detection +analyze_6ghz_environment() { + echo "" + echo "🔍 6GHz band analysis..." + + # Multiple detection methods for 6GHz capability + local supports_6ghz=false + local detection_method="" + + # Method 1: Standard iw phy Band 3 detection + SUPPORTS_6GHZ_BAND3=$(iw phy 2>/dev/null | grep -A 30 "Band 3:" | grep -E "freq.*6[0-9][0-9][0-9]") + + # Method 2: Alternative band detection (some drivers report differently) + SUPPORTS_6GHZ_ALT=$(iw phy 2>/dev/null | grep -E "freq.*6[0-9][0-9][0-9]") + + # Method 3: Check if we've actually connected to 6GHz before (proof positive) + CURRENT_FREQ_CHECK=false + if [ -n "$CURRENT_FREQ" ]; then + FREQ_INT=$(echo "$CURRENT_FREQ" | cut -d'.' -f1) + if [ "$FREQ_INT" -ge 6000 ] && [ "$FREQ_INT" -le 7200 ]; then + CURRENT_FREQ_CHECK=true + fi + fi + + # Method 4: Check for 6GHz frequencies in scan results (active proof) - FIXED + if [ -n "$SCAN_RESULTS" ]; then + SCAN_6GHZ=$(echo "$SCAN_RESULTS" | grep "freq:" | awk '$2 >= 6000 && $2 <= 7200' | wc -l) + else + # SCAN_RESULTS not available yet, do our own scan + LOCAL_SCAN_RESULTS=$(timeout 15 iw dev "$IFACE" scan 2>/dev/null) + SCAN_6GHZ=$(echo "$LOCAL_SCAN_RESULTS" | grep "freq:" | awk '$2 >= 6000 && $2 <= 7200' | wc -l) + fi + + # Method 5: Enhanced chip model detection (including PCI IDs) + CHIP_6GHZ_CAPABLE=false + if echo "$CHIP_MODEL" | grep -qi "mt7925\|mt7927"; then + CHIP_6GHZ_CAPABLE=true + elif echo "$CHIP_MODEL" | grep -qi "mt7922"; then + CHIP_6GHZ_CAPABLE=true + elif echo "$WIFI_HARDWARE" | grep -qi "device 0717\|device 0718\|device 0719"; then + # PCI device IDs for MT7925/MT7927 variants - these ARE 6GHz capable + CHIP_6GHZ_CAPABLE=true + elif echo "$CHIP_MODEL" | grep -qi "ax210\|ax211\|be200\|wcn7850"; then + CHIP_6GHZ_CAPABLE=true + fi + + # Method 6: Driver name detection (mt7925e driver indicates 6GHz capability) + DRIVER_6GHZ_CAPABLE=false + if echo "$DRIVER" | grep -qi "mt7925e"; then + DRIVER_6GHZ_CAPABLE=true + elif echo "$DRIVER" | grep -qi "iwlwifi" && echo "$CHIP_MODEL" | grep -qi "ax210\|ax211\|be200"; then + DRIVER_6GHZ_CAPABLE=true + fi + + # Determine 6GHz support using multiple evidence sources + if [ -n "$SUPPORTS_6GHZ_BAND3" ]; then + supports_6ghz=true + detection_method="iw phy Band 3" + elif [ -n "$SUPPORTS_6GHZ_ALT" ]; then + supports_6ghz=true + detection_method="frequency scan" + elif [ "$CURRENT_FREQ_CHECK" = true ]; then + supports_6ghz=true + detection_method="active 6GHz connection" + elif [ "$SCAN_6GHZ" -gt 0 ]; then + supports_6ghz=true + detection_method="6GHz networks detected ($SCAN_6GHZ found)" + elif [ "$CHIP_6GHZ_CAPABLE" = true ]; then + supports_6ghz=true + detection_method="Known 6GHz hardware: $CHIP_MODEL" + elif [ "$DRIVER_6GHZ_CAPABLE" = true ]; then + supports_6ghz=true + detection_method="6GHz-capable driver: $DRIVER" + fi + + # Report results + if [ "$supports_6ghz" = true ]; then + echo " ✅ Hardware: WiFi 6E/7 with 6GHz support confirmed" + echo " 🔍 Detection method: $detection_method" + + # Current connection analysis + if [ "$CURRENT_FREQ_CHECK" = true ]; then + echo " 🌟 Currently connected to 6GHz spectrum ($CURRENT_FREQ MHz)" + echo " 🚀 Excellent choice - 6GHz provides clean spectrum with minimal interference" + echo " ✅ 6GHz band: NO DFS channels - no radar interference possible" + else + echo " 📊 Currently on ${CURRENT_BAND:-unknown band}, but 6GHz available" + echo " 💡 Consider switching to 6GHz for cleaner spectrum" + echo " 🌟 6GHz advantage: NO DFS channels means no radar-related disconnections" + + # Enhanced troubleshooting for specific chips + if echo "$WIFI_HARDWARE" | grep -qi "device 0717"; then + echo " 🧪 MT7925 note: Latest WiFi 7 chip with full 6GHz support" + echo " 💡 Try: sudo iw reg set US && sudo nmcli radio wifi off && sudo nmcli radio wifi on" + echo " 💡 Or check router has 6GHz enabled and broadcasting" + elif echo "$CHIP_MODEL" | grep -qi "mt7922"; then + echo " 🧪 MT7922 note: 6GHz support may require regulatory domain setup" + echo " 💡 Try: sudo iw reg set US && sudo nmcli radio wifi off && sudo nmcli radio wifi on" + fi + fi + + # Show 6GHz network availability if scan worked - ENHANCED FIXED logic + if [ -n "$SCAN_6GHZ" ] && [ "$SCAN_6GHZ" -gt 0 ]; then + echo " 📡 Found $SCAN_6GHZ available 6GHz networks in area" + + # Show 6GHz network details if current connection is 6GHz + if [ "$CURRENT_FREQ_CHECK" = true ]; then + echo " 🌟 You're connected to one of these 6GHz networks!" + else + echo " 💡 Consider switching to 6GHz for access to these clean spectrum networks" + fi + else + echo " 📡 No 6GHz networks currently visible in detailed scan" + echo " 💡 Your hardware supports 6GHz, but no 6GHz networks detected in range" + echo " 💡 Router may need 6GHz enabled or be out of range" + fi + + else + echo " ❌ No 6GHz capability detected in hardware" + echo " 🧪 Note: Some WiFi 6E cards require specific firmware/driver versions" + + # Special case for known 6GHz hardware that isn't detected + if [ "$CHIP_6GHZ_CAPABLE" = true ] || [ "$DRIVER_6GHZ_CAPABLE" = true ]; then + echo " ⚠️ WARNING: Hardware/driver suggests 6GHz capability but not detected" + echo " 💡 Possible fixes:" + echo " • Update firmware: sudo apt update && sudo apt upgrade linux-firmware" + echo " • Set regulatory domain: sudo iw reg set US" + echo " • Check kernel version (6GHz requires 6.2+)" + echo " • Verify router has 6GHz enabled" + fi + fi +} + +# Enhanced system intelligence gathering with modern chipset support AND power save detection +gather_system_intelligence() { + echo -e "${CYAN}🧠 === ENHANCED SYSTEM INTELLIGENCE GATHERING ===${NC}" + + # Detect distribution first + detect_distribution + echo "🐧 Distribution: $DISTRO_NAME" + + # Get interface with fallback methods + IFACE=$(ip route get 1.1.1.1 2>/dev/null | grep dev | awk '{print $5}') + if [ -z "$IFACE" ]; then + IFACE=$(nmcli -t -f DEVICE,TYPE device status | grep wifi | head -1 | cut -d: -f1) + fi + if [ -z "$IFACE" ]; then + IFACE=$(iw dev 2>/dev/null | awk '/Interface/ {print $2; exit}') + fi + + echo "🔍 WiFi Interface: ${IFACE:-❌ Not found}" + + # Test WiFi functionality - SINGLE SOURCE OF TRUTH + WIFI_FUNCTIONAL=$(test_wifi_functionality) + echo "🔧 WiFi Functional Status: $([ "$WIFI_FUNCTIONAL" = "true" ] && echo "✅ WORKING" || echo "❌ BROKEN")" + + # Deep hardware analysis with modern chipset support + WIFI_HARDWARE=$(lspci | grep -i "network\|wireless" | head -1) + echo "🔧 Hardware: $WIFI_HARDWARE" + + # Analyze modern chipsets + analyze_modern_chipsets + + echo "📊 Chip Analysis:" + echo " Vendor: ${CHIP_VENDOR:-Unknown}" + echo " Model: ${CHIP_MODEL:-Unknown}" + echo " Generation: ${CHIP_GENERATION:-Unknown}" + echo " Known Issues: ${KNOWN_ISSUES:-Generally stable}" + + # Driver and firmware intelligence + if [ -n "$IFACE" ]; then + DRIVER=$(ls -l /sys/class/net/$IFACE/device/driver/module 2>/dev/null | awk -F/ '{print $NF}') + echo "🚗 Driver: ${DRIVER:-Unknown}" + + # Driver version and build info + if [ -n "$DRIVER" ]; then + DRIVER_VERSION=$(modinfo "$DRIVER" 2>/dev/null | grep "^version:" | awk '{print $2}') + DRIVER_DATE=$(modinfo "$DRIVER" 2>/dev/null | grep "^srcversion:" | awk '{print $2}') + echo " Version: ${DRIVER_VERSION:-Unknown}" + echo " Build ID: ${DRIVER_DATE:-Unknown}" + fi + + # Power Management Analysis - NEW ADDITION + echo "" + echo "🔋 Power Management Analysis:" + POWER_SAVE=$(iw dev "$IFACE" get power_save 2>/dev/null | grep "Power save:" | awk '{print $3}') + if [ -n "$POWER_SAVE" ]; then + case "$POWER_SAVE" in + "on") + echo " ⚠️ Power Save: ON (may cause disconnections)" + echo " 💡 Consider disabling: sudo iw dev $IFACE set power_save off" + ;; + "off") + echo " ✅ Power Save: OFF (optimal for stability)" + ;; + *) + echo " 🔍 Power Save: $POWER_SAVE" + ;; + esac + else + echo " ❓ Power Save: Unable to detect (driver may not support query)" + echo " 💡 Try manually: iw dev $IFACE get power_save" + fi + + # Check for ASPM status if MediaTek - FIXED to target only WiFi device + if echo "$CHIP_MODEL" | grep -qi "mt79"; then + # Get WiFi PCI device ID specifically + WIFI_PCI_ID=$(lspci | grep -i wireless | awk '{print $1}' | head -1) + if [ -n "$WIFI_PCI_ID" ]; then + ASMP_STATUS=$(lspci -vv -s "$WIFI_PCI_ID" 2>/dev/null | grep "LnkCtl:" | grep -o "ASPM [^;]*") + if [ -n "$ASMP_STATUS" ]; then + echo " 🔗 PCIe ASPM: $ASMP_STATUS" + if echo "$ASMP_STATUS" | grep -q "L1"; then + echo " 💡 ASPM L1 active - may cause MediaTek issues" + echo " 💡 Consider: pcie_aspm=off kernel parameter" + fi + fi + fi + fi + + # Distribution-aware firmware analysis + case "$DISTRO_ID" in + "fedora"|"rhel"|"centos"|"rocky"|"almalinux") + FIRMWARE_VERSION=$(rpm -q --queryformat '%{VERSION}-%{RELEASE}' linux-firmware 2>/dev/null || echo "Unknown") + ;; + "ubuntu"|"debian"|"pop"|"mint"|"linuxmint") + FIRMWARE_VERSION=$(dpkg -l linux-firmware 2>/dev/null | grep "^ii" | awk '{print $3}' || echo "Unknown") + ;; + "arch"|"manjaro"|"endeavouros") + FIRMWARE_VERSION=$(pacman -Q linux-firmware 2>/dev/null | awk '{print $2}' || echo "Unknown") + ;; + *) + FIRMWARE_VERSION="Unknown" + ;; + esac + + echo "" + echo "📦 Firmware: $FIRMWARE_VERSION" + + # Kernel compatibility check + KERNEL_VERSION=$(uname -r) + echo "🐧 Kernel: $KERNEL_VERSION" + + # Modern kernel recommendations + KERNEL_MAJOR=$(echo "$KERNEL_VERSION" | cut -d'.' -f1) + KERNEL_MINOR=$(echo "$KERNEL_VERSION" | cut -d'.' -f2) + + if [ "$KERNEL_MAJOR" -ge 6 ] && [ "$KERNEL_MINOR" -ge 8 ]; then + echo " ✅ Modern kernel with WiFi 7 support" + elif [ "$KERNEL_MAJOR" -ge 6 ] && [ "$KERNEL_MINOR" -ge 5 ]; then + echo " 📊 Good kernel with WiFi 6E support" + else + echo " ⚠️ Older kernel - consider upgrading for modern WiFi features" + fi + fi + + echo "" +} + +# Distribution-aware workaround system +provide_distribution_specific_workarounds() { + local issue_type="$1" + + echo "" + echo -e "${CYAN}🐧 Distribution-Specific Commands for $DISTRO_NAME:${NC}" + echo "" + + case "$issue_type" in + "DRIVER_ERROR") + echo "1. MediaTek-specific kernel parameter workaround:" + echo "" + # MediaTek-specific explanation + if echo "$CHIP_MODEL" | grep -qi "mt79"; then + echo -e "${GREEN}✅ MediaTek chipset detected: $CHIP_MODEL${NC}" + echo " 💡 Possible Fix When Drops Are Due to PCI Config Errors:" + echo " Some MediaTek chipsets (like mt7921e or mt7925e) suffer from unstable" + echo " PCIe behavior, especially on AMD-based laptops or quirky ACPI tables" + echo " (common in early Framework AMD laptops). If your dmesg/journal logs show" + echo " PCI-related errors (e.g. config read failures, bus enumeration issues)," + echo " pci=nommconf might stabilize the bus behavior." + echo "" + echo -e "${RED}❌ Won't Help for Firmware Bugs or ASPM Issues:${NC}" + echo " This won't fix issues caused by:" + echo " • ASPM power saving (pcie_aspm=off or policy=performance is needed)" + echo " • Firmware regressions (need linux-firmware downgrades or kernel patches)" + echo " • Power management instability (need iw dev ... set power_save off)" + echo " • DFS radar interference (need non-DFS channels)" + echo "" + echo " 📋 Check logs first: dmesg | grep -E 'mt79|pci.*error|config.*read'" + echo "" + else + echo " 💡 Generic PCI configuration workaround (may help with PCIe issues)" + echo "" + fi + + if [ "$DISTRO_ID" = "fedora" ] || [ "$DISTRO_ID" = "rhel" ]; then + echo " 🔒 PERMANENT (kernel parameter): $(get_distro_command "kernel_param")'pci=nommconf'" + echo " 🔒 PERMANENT (GRUB update): $(get_distro_command "grub_update")" + else + echo " 🔒 PERMANENT (kernel parameter): $(get_distro_command "kernel_param")'pci=nommconf'" + echo " 🔒 PERMANENT (GRUB update): $(get_distro_command "grub_update")" + fi + echo " ⚠️ REQUIRES REBOOT: sudo reboot" + echo "" + echo "2. Firmware update (try this first):" + echo " 🔒 PERMANENT (system upgrade): $(get_distro_command "firmware_update")" + echo " ⚠️ REQUIRES REBOOT: After dnf, apt, bootc/rpm-ostree upgrade" + echo "" + echo "3. Initrd rebuild:" + echo " 🔒 PERMANENT (initramfs update): $(get_distro_command "initrd_update")" + echo " ⚠️ REQUIRES REBOOT: sudo reboot" + echo "" + + # MediaTek-specific additional fixes + if echo "$CHIP_MODEL" | grep -qi "mt79"; then + echo "4. MediaTek-specific additional fixes:" + echo "" + echo " a) ASPM power management issues:" + echo " 🔒 PERMANENT (module config): echo 'options mt7921e disable_aspm=1' | sudo tee /etc/modprobe.d/mt7921e.conf" + echo " 🔒 PERMANENT (apply config): $(get_distro_command "initrd_update")" + echo " ⚠️ REQUIRES REBOOT: After initrd rebuild" + echo "" + echo " b) Power management workaround:" + echo " ⏰ TEMPORARY (until reboot): sudo iw dev $IFACE set power_save off" + echo " 🔒 PERMANENT: Use modprobe option above instead" + echo "" + echo " c) Alternative ASPM kernel parameter:" + echo " 🔒 PERMANENT (kernel param): $(get_distro_command "kernel_param")'pcie_aspm=off'" + echo " 🔒 PERMANENT (GRUB update): $(get_distro_command "grub_update")" + echo " ⚠️ REQUIRES REBOOT: After GRUB configuration" + echo "" + echo " 💡 Try solutions in order: firmware update → power_save off → ASPM fixes → PCI workarounds" + echo " 🧪 TESTING STRATEGY: Apply temporary fixes first to verify they work before making permanent" + fi + + # DFS-specific fixes if applicable + if [ "$DFS_IMPACT_SCORE" -gt 50 ]; then + echo "" + echo "5. DFS-specific fixes (radar interference detected):" + echo "" + echo " a) Immediate non-DFS channel switch:" + if [ -n "$CURRENT_SSID" ]; then + echo " ⏰ IMMEDIATE: sudo nmcli connection modify \"$CURRENT_SSID\" 802-11-wireless.band bg" + echo " ⏰ IMMEDIATE: sudo nmcli connection up \"$CURRENT_SSID\"" + fi + echo "" + echo " b) Router configuration (CRITICAL):" + echo " 🔒 PERMANENT: Set router to channels 36, 40, 44, 48 (non-DFS)" + echo " 🔒 PERMANENT: Alternative: channels 149, 153, 157, 161, 165 (non-DFS)" + echo " 🔒 PERMANENT: Disable automatic channel selection" + echo "" + echo " c) 6GHz migration (NO DFS in 6GHz):" + if [ "$SUPPORTS_WIFI7" = true ]; then + echo " 🌟 Your hardware supports 6GHz - upgrade to WiFi 6E/7 router" + echo " 💡 6GHz band is completely DFS-free" + fi + fi + ;; + "FIRMWARE_CRASH") + echo "1. Update firmware (most important for MediaTek):" + echo " 🔒 PERMANENT (system upgrade): $(get_distro_command "firmware_update")" + echo " ⚠️ REQUIRES REBOOT: After firmware update" + echo "" + + if echo "$CHIP_MODEL" | grep -qi "mt79"; then + echo "2. MediaTek-specific module configuration:" + echo " 🔒 PERMANENT (module config): echo 'options mt7921e disable_aspm=1' | sudo tee /etc/modprobe.d/mt7921e.conf" + echo " 🔒 PERMANENT (power config): echo 'options mt7921e power_save=0' | sudo tee -a /etc/modprobe.d/mt7921e.conf" + echo " 🔒 PERMANENT (apply config): $(get_distro_command "initrd_update")" + echo " ⚠️ REQUIRES REBOOT: After initrd rebuild" + else + echo "2. Module configuration:" + echo " 🔒 PERMANENT (module config): echo 'options $DRIVER disable_aspm=1' | sudo tee /etc/modprobe.d/$DRIVER.conf" + echo " 🔒 PERMANENT (apply config): $(get_distro_command "initrd_update")" + echo " ⚠️ REQUIRES REBOOT: After initrd rebuild" + fi + ;; + esac +} + +# TX Power Band Test - COMPLETE IMPLEMENTATION +tx_power_band_test() { + echo -e "${BOLD}${BLUE}🧪 === TX POWER BAND TEST ===${NC}" + echo "🔍 Testing transmission power limitations across different bands" + echo "" + + # Quick system check first + gather_system_intelligence >/dev/null 2>&1 + + if [ -z "$IFACE" ]; then + echo "❌ No WiFi interface available for testing" + return 1 + fi + + echo "🎯 Current System Analysis:" + echo " Interface: $IFACE" + echo " Chip: ${CHIP_VENDOR:-Unknown} ${CHIP_MODEL:-Unknown}" + echo " Driver: ${DRIVER:-Unknown}" + echo "" + + # Get current TX power + CURRENT_TX_POWER=$(iw dev "$IFACE" info 2>/dev/null | grep "txpower" | awk '{print $2, $3}') + echo "📊 Current TX Power: ${CURRENT_TX_POWER:-Unable to detect}" + echo "" + + # Test TX power changes on different power levels + echo "🧪 Testing TX power control capabilities:" + echo "" + + # Store original power for restoration + echo "" + echo "🎯 Band-specific TX Power Analysis:" + echo "" + + # Get current connection info for band analysis + WIFI_INFO=$(iw dev "$IFACE" link 2>/dev/null) + if echo "$WIFI_INFO" | grep -q "Connected to"; then + CURRENT_FREQ=$(echo "$WIFI_INFO" | grep "freq:" | awk '{print $2}') + CURRENT_SIGNAL=$(echo "$WIFI_INFO" | grep "signal:" | awk '{print $2}') + + if [ -n "$CURRENT_FREQ" ]; then + FREQ_INT=$(echo "$CURRENT_FREQ" | cut -d'.' -f1) + + if [ "$FREQ_INT" -ge 2400 ] && [ "$FREQ_INT" -le 2500 ]; then + CURRENT_BAND="2.4 GHz" + echo "📡 Currently on 2.4GHz band:" + echo " • Typical max TX power: 20dBm (100mW)" + echo " • Range: Good penetration through walls" + echo " • Congestion: Usually high in urban areas" + elif [ "$FREQ_INT" -ge 5000 ] && [ "$FREQ_INT" -le 6000 ]; then + CURRENT_BAND="5 GHz" + echo "📡 Currently on 5GHz band:" + echo " • Typical max TX power: 20-23dBm (100-200mW)" + echo " • Range: Shorter than 2.4GHz but less congested" + echo " • DFS channels: May have radar interference" + elif [ "$FREQ_INT" -ge 6000 ] && [ "$FREQ_INT" -le 7200 ]; then + CURRENT_BAND="6 GHz" + echo "📡 Currently on 6GHz band (WiFi 6E/7):" + echo " • Typical max TX power: 20dBm (100mW)" + echo " • Range: Shortest but cleanest spectrum" + echo " • DFS channels: NONE - completely DFS-free!" + fi + + echo " • Current frequency: $CURRENT_FREQ MHz" + echo " • Current signal: ${CURRENT_SIGNAL:-Unknown} dBm" + fi + else + echo "❌ Not connected - cannot analyze current band" + fi + + echo "" + echo "🎯 Regulatory Domain Impact:" + echo "" + + REG_INFO=$(iw reg get 2>/dev/null) + if [ -n "$REG_INFO" ]; then + REG_COUNTRY=$(echo "$REG_INFO" | grep "country" | head -1 | awk '{print $2}' | tr -d ':') + echo "📍 Current regulatory domain: ${REG_COUNTRY:-Global}" + + case "$REG_COUNTRY" in + "US") + echo " • 2.4GHz: Max 30dBm EIRP (1W)" + echo " • 5GHz low: Max 30dBm EIRP (1W)" + echo " • 5GHz high: Max 30dBm EIRP (1W)" + echo " • 6GHz: Max 30dBm EIRP (1W)" + ;; + "EU"|"DE"|"FR"|"GB") + echo " • 2.4GHz: Max 20dBm EIRP (100mW)" + echo " • 5GHz: Max 23dBm EIRP (200mW)" + echo " • 6GHz: Max 23dBm EIRP (200mW)" + ;; + "JP") + echo " • 2.4GHz: Max 20dBm EIRP (100mW)" + echo " • 5GHz: Max 20dBm EIRP (100mW)" + echo " • 6GHz: Varies by channel" + ;; + *) + echo " • Check local regulations for your country" + echo " • Set correct domain: sudo iw reg set [COUNTRY_CODE]" + ;; + esac + else + echo "❓ Cannot determine regulatory domain" + echo "💡 Set manually: sudo iw reg set US (or your country code)" + fi + + echo "" + echo "🎯 Chipset-Specific TX Power Behavior:" + echo "" + + case "$CHIP_VENDOR" in + "MediaTek") + echo "📊 MediaTek chipset behavior:" + echo " • Manual TX power: Often ignored by driver" + echo " • Automatic control: Usually works well" + echo " • Regional limits: Strictly enforced" + echo " • Recommendation: Use auto mode, optimize router-side" + ;; + "Intel") + echo "📊 Intel chipset behavior:" + echo " • Manual TX power: Limited by regulatory database" + echo " • Automatic control: Excellent dynamic adjustment" + echo " • Regional limits: Strictly enforced" + echo " • Recommendation: Ensure correct regulatory domain" + ;; + "Qualcomm") + echo "📊 Qualcomm chipset behavior:" + echo " • Manual TX power: Better support than MediaTek" + echo " • Automatic control: Good" + echo " • Regional limits: Enforced" + echo " • Recommendation: Manual adjustment may work" + ;; + *) + echo "📊 Generic chipset recommendations:" + echo " • Try auto mode first" + echo " • Check regulatory domain setting" + echo " • Monitor for driver restrictions" + ;; + esac + + echo "" + echo "💡 TX Power Optimization Recommendations:" + echo "" + echo "1. For better range:" + echo " • Ensure correct regulatory domain: sudo iw reg set [COUNTRY]" + echo " • Optimize router TX power settings" + echo " • Consider external antennas if supported" + echo "" + echo "2. For MediaTek cards specifically:" + echo " • Router-side power optimization more effective" + echo " • Use 2.4GHz for maximum range" + echo " • Avoid DFS channels (radar interference)" + echo "" + echo "3. For 6GHz optimization:" + if [ "$SUPPORTS_WIFI7" = true ]; then + echo " ✅ Your hardware supports 6GHz" + echo " • 6GHz = shortest range but cleanest spectrum" + echo " • NO DFS interference possible" + echo " • Best for high-speed, short-distance connections" + else + echo " 💡 Upgrade to WiFi 6E/7 for 6GHz access" + echo " • 6GHz provides DFS-free operation" + echo " • Clean spectrum with minimal interference" + fi + + echo "" + echo "📋 Quick Commands for Testing:" + echo " • Check current power: iw dev $IFACE info | grep txpower" + echo " • Test signal strength: watch -n 1 'iw dev $IFACE link | grep signal'" + echo " • Speed test: speedtest-cli" + echo "" +} + +# Manual Band Switching - COMPLETE IMPLEMENTATION +manual_band_switching() { + echo -e "${BOLD}${GREEN}📡 === MANUAL BAND SWITCHING ===${NC}" + echo "🎯 Direct CLI commands for switching between WiFi bands" + echo "" + + # Quick system check + gather_system_intelligence >/dev/null 2>&1 + + if [ -z "$IFACE" ]; then + echo "❌ No WiFi interface available" + return 1 + fi + + # Get current connection + ACTIVE_CONNECTION=$(nmcli -t connection show --active | grep -E "wifi|802-11-wireless" | head -1 | cut -d: -f1) + + if [ -z "$ACTIVE_CONNECTION" ]; then + echo "❌ No active WiFi connection detected" + echo "💡 Connect to WiFi first, then run band switching" + return 1 + fi + + echo "📡 Current active connection: $ACTIVE_CONNECTION" + + # Get current connection details + CURRENT_FREQ=$(iw dev "$IFACE" link 2>/dev/null | grep "freq:" | awk '{print $2}') + CURRENT_SIGNAL=$(iw dev "$IFACE" link 2>/dev/null | grep "signal:" | awk '{print $2}') + + if [ -n "$CURRENT_FREQ" ]; then + FREQ_INT=$(echo "$CURRENT_FREQ" | cut -d'.' -f1) + + if [ "$FREQ_INT" -ge 2400 ] && [ "$FREQ_INT" -le 2500 ]; then + CURRENT_BAND_DISPLAY="2.4 GHz" + elif [ "$FREQ_INT" -ge 5000 ] && [ "$FREQ_INT" -le 6000 ]; then + CURRENT_BAND_DISPLAY="5 GHz" + elif [ "$FREQ_INT" -ge 6000 ] && [ "$FREQ_INT" -le 7200 ]; then + CURRENT_BAND_DISPLAY="6 GHz (WiFi 6E/7)" + else + CURRENT_BAND_DISPLAY="Unknown" + fi + else + CURRENT_BAND_DISPLAY="Unknown" + fi + + echo "📊 Current status:" + echo " Band: $CURRENT_BAND_DISPLAY" + echo " Frequency: ${CURRENT_FREQ:-Unknown} MHz" + echo " Signal: ${CURRENT_SIGNAL:-Unknown} dBm" + echo "" + + echo -e "${CYAN}🎯 === BAND SWITCHING COMMANDS ===${NC}" + echo "" + + echo "1) 🔵 Force 2.4GHz Band (Maximum Range)" + echo " Command: sudo nmcli connection modify \"$ACTIVE_CONNECTION\" 802-11-wireless.band bg" + echo " Effect: Forces connection to 2.4GHz only" + echo " Use for: Maximum range, penetration through walls" + echo " DFS risk: ZERO (no DFS channels in 2.4GHz)" + echo "" + + echo "2) 🟢 Force 5GHz Band (Balanced Performance)" + echo " Command: sudo nmcli connection modify \"$ACTIVE_CONNECTION\" 802-11-wireless.band a" + echo " Effect: Forces connection to 5GHz only" + echo " Use for: Better speed, less congestion" + echo " DFS risk: MEDIUM (some 5GHz channels use DFS)" + echo "" + + echo "3) 🟡 Auto Band Selection (Default)" + echo " Command: sudo nmcli connection modify \"$ACTIVE_CONNECTION\" 802-11-wireless.band ''" + echo " Effect: Allows automatic band selection" + echo " Use for: Let system choose best band" + echo " DFS risk: VARIES (depends on router channel selection)" + echo "" + + # 6GHz options only if supported + if [ "$SUPPORTS_WIFI7" = true ] || echo "$CHIP_MODEL" | grep -qi "6E"; then + echo "4) 🔴 6GHz Band Access (WiFi 6E/7 - Clean Spectrum)" + echo " Note: 6GHz typically included in 5GHz 'a' band setting" + echo " Command: sudo nmcli connection modify \"$ACTIVE_CONNECTION\" 802-11-wireless.band a" + echo " Requirement: Router must broadcast 6GHz SSID" + echo " DFS risk: ZERO (NO DFS channels exist in 6GHz)" + echo " 💡 6GHz advantages: Clean spectrum, no radar interference" + echo "" + fi + + echo -e "${CYAN}🔧 === SPECIFIC CHANNEL SELECTION ===${NC}" + echo "" + + echo "Safe 5GHz Channels (NO DFS - No Radar Interference):" + echo " Low band: 36, 40, 44, 48" + echo " • Channel 36: sudo nmcli connection modify \"$ACTIVE_CONNECTION\" 802-11-wireless.channel 36" + echo " • Channel 44: sudo nmcli connection modify \"$ACTIVE_CONNECTION\" 802-11-wireless.channel 44" + echo "" + echo " High band: 149, 153, 157, 161, 165" + echo " • Channel 149: sudo nmcli connection modify \"$ACTIVE_CONNECTION\" 802-11-wireless.channel 149" + echo " • Channel 157: sudo nmcli connection modify \"$ACTIVE_CONNECTION\" 802-11-wireless.channel 157" + echo "" + + echo "⚠️ Avoid These Channels (DFS - Radar Interference Risk):" + echo " DFS channels: 52, 56, 60, 64, 100-144" + echo " 💡 These channels can cause sudden 30+ second disconnections" + echo "" + + echo -e "${CYAN}🧪 === INTERACTIVE BAND SWITCHING ===${NC}" + echo "" + echo "Would you like to switch bands now? [y/N]" + read -r switch_choice + + if [[ "$switch_choice" =~ ^[Yy]$ ]]; then + echo "" + echo "Select band to switch to:" + echo "1) 2.4GHz (maximum range, DFS-free)" + echo "2) 5GHz (balanced performance, some DFS risk)" + echo "3) Auto selection (system chooses)" + if [ "$SUPPORTS_WIFI7" = true ]; then + echo "4) Force specific safe channel (recommended)" + fi + echo "5) Cancel" + echo "" + echo -n "Choice [1-5]: " + read -r band_choice + + case $band_choice in + 1) + echo "🔵 Switching to 2.4GHz band..." + if sudo nmcli connection modify "$ACTIVE_CONNECTION" 802-11-wireless.band bg; then + sudo nmcli connection up "$ACTIVE_CONNECTION" + echo "✅ Switched to 2.4GHz band" + echo "💡 This provides maximum range and is completely DFS-free" + else + echo "❌ Failed to switch to 2.4GHz" + fi + ;; + 2) + echo "🟢 Switching to 5GHz band..." + if sudo nmcli connection modify "$ACTIVE_CONNECTION" 802-11-wireless.band a; then + sudo nmcli connection up "$ACTIVE_CONNECTION" + echo "✅ Switched to 5GHz band" + echo "⚠️ Monitor for DFS-related disconnections" + else + echo "❌ Failed to switch to 5GHz" + fi + ;; + 3) + echo "🟡 Enabling automatic band selection..." + if sudo nmcli connection modify "$ACTIVE_CONNECTION" 802-11-wireless.band ""; then + sudo nmcli connection up "$ACTIVE_CONNECTION" + echo "✅ Enabled automatic band selection" + else + echo "❌ Failed to enable auto selection" + fi + ;; + 4) + if [ "$SUPPORTS_WIFI7" = true ]; then + # Detect network type (mesh, hotspot, single-band) + MESH_DETECTED=false + HOTSPOT_DETECTED=false + SUPPORTS_5GHZ=true + SUPPORTS_24GHZ=true + + # Get current SSID if not already set +if [ -z "$CURRENT_SSID" ]; then + CURRENT_SSID=$(iw dev "$IFACE" link 2>/dev/null | grep "SSID:" | awk '{print $2}') + if [ -z "$CURRENT_SSID" ]; then + CURRENT_SSID=$(nmcli -t -f active,ssid dev wifi | grep '^yes' | cut -d: -f2) + fi +fi + +# Check for hotspots and band capabilities FIRST +if echo "$CURRENT_SSID" | grep -qi "iphone\|android\|hotspot\|mobile\|pixel_\|galaxy_\|oneplus_\|xiaomi_\|huawei_\|samsung_\|nokia_\|motorola_\|lg_\|sony_\|oppo_\|vivo_\|realme_\|htc_" || \ + echo "$CURRENT_SSID" | grep -qE "^[A-Za-z]+_[0-9]{3,4}$" || \ + echo "$CURRENT_SSID" | grep -qE "^[A-Za-z]+'s (iPhone|Android|Phone)" || \ + echo "$CURRENT_SSID" | grep -qi "phone\|tether\|share\|wifi.*direct\|portable.*wifi"; then + HOTSPOT_DETECTED=true +fi + +# Check for mesh networks (improved logic - after hotspot detection) +if echo "$CURRENT_SSID" | grep -qi "eero\|orbi\|deco\|velop\|amplifi\|nest"; then + MESH_DETECTED=true +elif [ "$HOTSPOT_DETECTED" = false ]; then + # Only consider it mesh if we find MANY similar SSIDs (3+ nodes typical for mesh) + BASE_SSID=$(echo "$CURRENT_SSID" | sed 's/_5G\|_2G\|_6G\|-5G\|-2G\|-6G//') + SIMILAR_SSID_COUNT=$(nmcli -t -f SSID dev wifi | grep -c "$BASE_SSID" 2>/dev/null || echo "0") + + # Mesh networks typically have 3+ nodes broadcasting + if [ "$SIMILAR_SSID_COUNT" -ge 3 ]; then + MESH_DETECTED=true + fi +fi + + # Get scan results if not already available +if [ -z "$SCAN_RESULTS" ]; then + echo "🔍 Scanning for available bands..." + SCAN_RESULTS=$(timeout 15 iw dev "$IFACE" scan 2>/dev/null) +fi + +# For connected networks, use current connection info instead of scan +CURRENT_CONNECTION_FREQ=$(iw dev "$IFACE" link 2>/dev/null | grep "freq:" | awk '{print $2}') + +# Check actual band availability for this network +if [ -n "$CURRENT_CONNECTION_FREQ" ]; then + # We're connected - use current connection frequency to determine bands + FREQ_INT=$(echo "$CURRENT_CONNECTION_FREQ" | cut -d'.' -f1) + + if [ "$FREQ_INT" -ge 2400 ] && [ "$FREQ_INT" -le 2500 ]; then + # Connected on 2.4GHz - this network definitely supports 2.4GHz + SUPPORTS_24GHZ=true + # Check if we can find 5GHz for this SSID in scan results + if ! echo "$SCAN_RESULTS" | grep -A1 "SSID: $CURRENT_SSID" | grep -q "freq: 5[0-9][0-9][0-9]"; then + SUPPORTS_5GHZ=false + fi + elif [ "$FREQ_INT" -ge 5000 ] && [ "$FREQ_INT" -le 6000 ]; then + # Connected on 5GHz - this network definitely supports 5GHz + SUPPORTS_5GHZ=true + # Check if we can find 2.4GHz for this SSID in scan results + if ! echo "$SCAN_RESULTS" | grep -A1 "SSID: $CURRENT_SSID" | grep -q "freq: 24[0-9][0-9]"; then + SUPPORTS_24GHZ=false + fi + fi +elif [ -n "$SCAN_RESULTS" ]; then + # Not connected - use scan results + if ! echo "$SCAN_RESULTS" | grep -A1 "SSID: $CURRENT_SSID" | grep -q "freq: 5[0-9][0-9][0-9]"; then + SUPPORTS_5GHZ=false + fi + if ! echo "$SCAN_RESULTS" | grep -A1 "SSID: $CURRENT_SSID" | grep -q "freq: 24[0-9][0-9]"; then + SUPPORTS_24GHZ=false + fi +fi + + # Handle single-band networks first + if [ "$SUPPORTS_5GHZ" = false ] && [ "$SUPPORTS_24GHZ" = true ]; then + echo "" + echo "📱 2.4GHz-ONLY NETWORK DETECTED" + if [ "$HOTSPOT_DETECTED" = true ]; then + echo "(Likely mobile hotspot - most only support 2.4GHz)" + fi + echo "" + echo "This network only broadcasts on 2.4GHz band." + echo "5GHz switching is not available for this network." + echo "" + echo "Available options:" + echo "1) Stay on 2.4GHz (only option for this network)" + echo "2) Reset to auto" + echo "" + echo -n "Choose option [1-2]: " + read -r single_band_choice + + case $single_band_choice in + 1) + echo "🔵 Confirming 2.4GHz connection..." + if sudo nmcli connection modify "$ACTIVE_CONNECTION" 802-11-wireless.band bg; then + sudo nmcli connection up "$ACTIVE_CONNECTION" + echo "✅ Confirmed 2.4GHz connection (only available band)" + else + echo "❌ Failed to confirm 2.4GHz connection" + fi + ;; + 2) + echo "🔄 Resetting to auto..." + if sudo nmcli connection modify "$ACTIVE_CONNECTION" 802-11-wireless.band ""; then + sudo nmcli connection up "$ACTIVE_CONNECTION" + echo "✅ Reset to auto (will use 2.4GHz anyway)" + else + echo "❌ Failed to reset to auto" + fi + ;; + *) + echo "❌ Invalid choice" + ;; + esac + elif [ "$SUPPORTS_24GHZ" = false ] && [ "$SUPPORTS_5GHZ" = true ]; then + echo "" + echo "📡 5GHz-ONLY NETWORK DETECTED" + echo "" + echo "This network only broadcasts on 5GHz band." + echo "2.4GHz switching is not available for this network." + echo "" + echo "Available options:" + echo "1) Stay on 5GHz (only option for this network)" + echo "2) Reset to auto" + echo "" + echo -n "Choose option [1-2]: " + read -r single_band_choice + + case $single_band_choice in + 1) + echo "🟢 Confirming 5GHz connection..." + if sudo nmcli connection modify "$ACTIVE_CONNECTION" 802-11-wireless.band a; then + sudo nmcli connection up "$ACTIVE_CONNECTION" + echo "✅ Confirmed 5GHz connection (only available band)" + else + echo "❌ Failed to confirm 5GHz connection" + fi + ;; + 2) + echo "🔄 Resetting to auto..." + if sudo nmcli connection modify "$ACTIVE_CONNECTION" 802-11-wireless.band ""; then + sudo nmcli connection up "$ACTIVE_CONNECTION" + echo "✅ Reset to auto (will use 5GHz anyway)" + else + echo "❌ Failed to reset to auto" + fi + ;; + *) + echo "❌ Invalid choice" + ;; + esac + elif [ "$MESH_DETECTED" = true ]; then + echo "" + echo "🌐 MESH NETWORK DETECTED - Modified approach:" + echo "" + echo "Mesh networks manage channels automatically. Instead of forcing" + echo "specific channels, try these mesh-friendly approaches:" + echo "" + echo "1) Force 2.4GHz (mesh usually has dedicated 2.4GHz nodes)" + echo "2) Force 5GHz (let mesh choose best 5GHz channel)" + echo "3) Reset to auto (recommended for mesh)" + echo "" + echo -n "Choose approach [1-3]: " + read -r mesh_choice + + case $mesh_choice in + 1) + echo "🔵 Forcing 2.4GHz for mesh compatibility..." + if sudo nmcli connection modify "$ACTIVE_CONNECTION" 802-11-wireless.band bg; then + sudo nmcli connection up "$ACTIVE_CONNECTION" + echo "✅ Switched to 2.4GHz (mesh will choose optimal 2.4GHz channel)" + else + echo "❌ Failed to switch to 2.4GHz" + fi + ;; + 2) + echo "🟢 Forcing 5GHz for mesh compatibility..." + if sudo nmcli connection modify "$ACTIVE_CONNECTION" 802-11-wireless.band a; then + sudo nmcli connection up "$ACTIVE_CONNECTION" + echo "✅ Switched to 5GHz (mesh will choose optimal 5GHz channel)" + else + echo "❌ Failed to switch to 5GHz" + fi + ;; + 3) + echo "🔄 Resetting to auto for optimal mesh performance..." + if sudo nmcli connection modify "$ACTIVE_CONNECTION" 802-11-wireless.band "" && \ + sudo nmcli connection modify "$ACTIVE_CONNECTION" 802-11-wireless.channel ""; then + sudo nmcli connection up "$ACTIVE_CONNECTION" + echo "✅ Reset to auto (recommended for mesh networks)" + else + echo "❌ Failed to reset to auto" + fi + ;; + *) + echo "❌ Invalid choice" + ;; + esac + else + echo "" + echo "Select safe channel (no DFS):" + echo "36) Channel 36 (5180 MHz, safe)" + echo "44) Channel 44 (5220 MHz, safe)" + echo "149) Channel 149 (5745 MHz, safe)" + echo "157) Channel 157 (5785 MHz, safe)" + echo "" + echo -n "Enter channel number: " + read -r channel_choice + + if [[ "$channel_choice" =~ ^(36|44|149|157)$ ]]; then + echo "🔧 Switching to channel $channel_choice..." + # FIXED: Set band FIRST, then channel + if sudo nmcli connection modify "$ACTIVE_CONNECTION" 802-11-wireless.band a && \ + sudo nmcli connection modify "$ACTIVE_CONNECTION" 802-11-wireless.channel "$channel_choice"; then + sudo nmcli connection up "$ACTIVE_CONNECTION" + echo "✅ Switched to channel $channel_choice (5GHz, DFS-free)" + else + echo "❌ Failed to switch to channel $channel_choice" + echo "💡 Try: Set band first, then channel manually" + fi + else + echo "❌ Invalid channel. Use: 36, 44, 149, or 157" + fi + fi + fi + ;; + 5) + echo "Cancelled - no changes made" + ;; + *) + echo "❌ Invalid option" + ;; + esac + + if [[ "$band_choice" =~ ^[1-4]$ ]]; then + echo "" + echo "🔍 Waiting 10 seconds for connection to stabilize..." + sleep 10 + echo "" + echo "📊 New connection status:" + NEW_FREQ=$(iw dev "$IFACE" link 2>/dev/null | grep "freq:" | awk '{print $2}') + NEW_SIGNAL=$(iw dev "$IFACE" link 2>/dev/null | grep "signal:" | awk '{print $2}') + + if [ -n "$NEW_FREQ" ]; then + NEW_FREQ_INT=$(echo "$NEW_FREQ" | cut -d'.' -f1) + + if [ "$NEW_FREQ_INT" -ge 2400 ] && [ "$NEW_FREQ_INT" -le 2500 ]; then + NEW_BAND="2.4 GHz" + elif [ "$NEW_FREQ_INT" -ge 5000 ] && [ "$NEW_FREQ_INT" -le 6000 ]; then + NEW_BAND="5 GHz" + elif [ "$NEW_FREQ_INT" -ge 6000 ] && [ "$NEW_FREQ_INT" -le 7200 ]; then + NEW_BAND="6 GHz" + else + NEW_BAND="Unknown" + fi + + echo " New band: $NEW_BAND" + echo " New frequency: $NEW_FREQ MHz" + echo " New signal: ${NEW_SIGNAL:-Unknown} dBm" + + # DFS analysis for new connection + if [ "$NEW_BAND" = "5 GHz" ]; then + NEW_CHANNEL=$(echo "scale=0; ($NEW_FREQ_INT - 5000) / 5" | bc 2>/dev/null || echo "Unknown") + if [ "$NEW_CHANNEL" != "Unknown" ] && is_dfs_channel "$NEW_CHANNEL"; then + echo -e " ${YELLOW}⚠️ DFS Warning: Connected to DFS channel $NEW_CHANNEL${NC}" + echo " 💡 Monitor for sudden disconnections lasting 30+ seconds" + else + echo -e " ${GREEN}✅ Safe channel: $NEW_CHANNEL (non-DFS)${NC}" + fi + fi + else + echo " ❌ Connection status not available" + fi + fi + fi + + echo "" + echo -e "${GREEN}💡 Band Switching Tips:${NC}" + echo "• 2.4GHz: Best range, completely DFS-free, but often congested" + echo "• 5GHz: Good balance, but check for DFS channel usage" + echo "• 6GHz: Shortest range but cleanest spectrum (NO DFS ever)" + echo "• Safe 5GHz channels: 36, 40, 44, 48, 149, 153, 157, 161, 165" + echo "• Avoid DFS channels: 52-64, 100-144 (radar interference risk)" + echo "" + echo "📋 Useful monitoring commands:" + echo "• Current status: iw dev $IFACE link" + echo "• Signal monitoring: watch -n 2 'iw dev $IFACE link | grep signal'" + echo "• Speed test: speedtest-cli" + echo "" +} + +# Enhanced menu system - COMPLETE WITH ALL OPTIONS +show_menu() { + clear + echo -e "${BOLD}${CYAN}🧠 === ENHANCED WiFi ANALYZER ===${NC}" + echo -e "${BOLD}WiFi 7 • MLO • 6GHz • DFS Monitoring • Distribution Adaptive • Modern VPN Support${NC}" + echo "" + echo "1) 🎯 Complete WiFi Analysis (All-in-One with WiFi 7 + DFS support)" + echo "2) 🚨 Error Analysis & Troubleshooting (Distribution-aware fixes + DFS)" + echo "3) 🛠️ Interactive Workaround Generator (Modern solutions + DFS fixes)" + echo "4) 📡 DFS Channel Monitor (Dedicated radar interference analysis)" + echo "5) 🧪 TX Power Band Test (diagnose power limitations)" + echo "6) 📡 Manual Band Switching (2.4GHz/5GHz/6GHz CLI commands)" + echo "7) 🚪 Exit" + echo "" + echo -n "Select option [1-7]: " +} + +# Complete comprehensive analysis with modern features and DFS monitoring +complete_wifi_analysis() { + echo -e "${BOLD}${BLUE}🧠 === COMPLETE WiFi ANALYSIS ===${NC}" + echo "🔬 WiFi 7 • MLO • 6GHz • DFS Monitoring • Modern VPN • Distribution adaptive" + echo "📊 Comprehensive analysis for modern WiFi systems with radar interference detection" + echo "" + + # Run all analysis components + gather_system_intelligence + detect_vpn_configuration + analyze_rf_frequency_environment # This now includes DFS analysis + detect_severe_wifi_issues # This now includes DFS-aware detection + + # Provide DFS recommendations if relevant + if [ "$DFS_IMPACT_SCORE" -gt 25 ]; then + echo "" + provide_dfs_recommendations + fi + + # Final summary with modern features and DFS + echo -e "${CYAN}🎯 === FINAL ANALYSIS SUMMARY ===${NC}" + echo "" + + OVERALL_HEALTH="Excellent" + HEALTH_SCORE=100 + + # Calculate overall health including DFS impact + if [ "$WIFI_FUNCTIONAL" != "true" ]; then + OVERALL_HEALTH="Critical" + HEALTH_SCORE=0 + elif [ "$SEVERE_ISSUES" -gt 0 ]; then + OVERALL_HEALTH="Poor" + HEALTH_SCORE=25 + elif [ "$DFS_IMPACT_SCORE" -gt 75 ]; then + OVERALL_HEALTH="Poor (DFS interference)" + HEALTH_SCORE=30 + elif [ "$DFS_IMPACT_SCORE" -gt 50 ]; then + OVERALL_HEALTH="Fair (DFS risk)" + HEALTH_SCORE=50 + elif [ "$VPN_IMPACT_SCORE" -gt 50 ]; then + OVERALL_HEALTH="Fair" + HEALTH_SCORE=60 + elif [ "$DFS_IMPACT_SCORE" -gt 25 ]; then + HEALTH_SCORE=85 # Minor DFS impact + fi + + echo "📊 Overall WiFi Health: $OVERALL_HEALTH ($HEALTH_SCORE/100)" + echo "🔧 WiFi Status: $([ "$WIFI_FUNCTIONAL" = "true" ] && echo "✅ Working" || echo "❌ Broken")" + echo "🔒 VPN Status: $VPN_ACTIVE ($VPN_TYPE)" + echo "📡 Hardware: ${CHIP_VENDOR:-Unknown} ${CHIP_MODEL:-Unknown} (${CHIP_GENERATION:-Unknown})" + echo "🐧 System: $DISTRO_NAME" + + # DFS status summary + if [ "$DFS_CURRENT_CONNECTION" = true ]; then + echo "📡 DFS Status: ⚠️ Connected to DFS channel (radar risk)" + elif [ "$DFS_COUNT" -gt 0 ]; then + echo "📡 DFS Status: 📊 $DFS_COUNT DFS networks in area" + else + echo "📡 DFS Status: ✅ Clean environment (no DFS detected)" + fi + + # Modern features summary + if [ "$SUPPORTS_WIFI7" = true ]; then + echo "🌟 WiFi 7 Features: $([ "$SUPPORTS_MLO" = true ] && echo "MLO capable" || echo "Standard WiFi 7")" + fi + + if [ "$WIFI_FUNCTIONAL" = "true" ]; then + echo "" + echo -e "${GREEN}✅ SYSTEM STATUS: HEALTHY${NC}" + echo "🎉 Your modern WiFi system is functioning properly" + if [ "$VPN_ACTIVE" = "Yes" ]; then + echo "🔒 Modern VPN integration appears stable" + fi + + # Modern optimization recommendations + if [ "$SUPPORTS_WIFI7" = true ] && [ "$CURRENT_BAND" != "6 GHz (WiFi 6E/7)" ]; then + echo "💡 Optimization: Consider upgrading to WiFi 7 router for 6GHz access" + echo "💡 6GHz Advantage: No DFS channels = no radar interference" + fi + if [ "$SUPPORTS_MLO" = true ]; then + echo "🔗 MLO available: Multi-link operation can improve performance" + fi + + # DFS-specific recommendations + if [ "$DFS_CURRENT_CONNECTION" = true ]; then + echo "" + echo -e "${YELLOW}⚠️ DFS RECOMMENDATION: Switch to non-DFS channel for stability${NC}" + echo "💡 Suggested channels: 36, 40, 44, 48 (low 5GHz) or 149+ (high 5GHz)" + elif [ "$DFS_IMPACT_SCORE" -gt 50 ]; then + echo "" + echo -e "${YELLOW}⚠️ DFS ENVIRONMENT: High radar activity detected${NC}" + echo "💡 Monitor for disconnection patterns and consider non-DFS channels" + fi + + else + echo "" + echo -e "${RED}🚨 SYSTEM STATUS: NEEDS ATTENTION${NC}" + echo "💡 Run option 2 for detailed error analysis and troubleshooting" + + if [ "$DFS_IMPACT_SCORE" -gt 50 ]; then + echo "" + echo -e "${MAGENTA}📡 DFS FACTOR: Radar interference may be contributing to issues${NC}" + echo "💡 Try non-DFS channels first before other troubleshooting" + fi + fi + + echo "" +} + +# Error analysis and troubleshooting with distribution awareness and DFS +error_analysis_troubleshooting() { + echo -e "${BOLD}${RED}🚨 === ERROR ANALYSIS & TROUBLESHOOTING ===${NC}" + echo "🔍 Deep dive into system logs, errors, and failure patterns" + echo "🛠️ Distribution-aware troubleshooting recommendations with DFS analysis" + echo "" + + # Quick system check first + gather_system_intelligence + + # Run DFS analysis early to identify radar-related issues + analyze_dfs_channels >/dev/null 2>&1 + + # NEW: Enhanced authentication failure detection + AUTH_ISSUES=0 + AUTH_FAILURES=$(journalctl --since "24 hours ago" --no-pager 2>/dev/null | \ + grep -E "association took too long|ssid-not-found|authenticating.*disconnected" | \ + wc -l) + + if [ "$AUTH_FAILURES" -gt 0 ]; then + echo -e "${RED}🚨 FOUND $AUTH_FAILURES AUTHENTICATION FAILURES${NC}" + echo "" + echo "📋 Recent authentication failure patterns:" + journalctl --since "6 hours ago" --no-pager 2>/dev/null | \ + grep -E "association took too long|ssid-not-found|authenticating.*disconnected" | \ + tail -5 | while IFS= read -r line; do + echo " $line" + done + echo "" + AUTH_ISSUES=1 + fi + + # Check for DFS-related disconnection patterns in logs + DFS_LOG_ISSUES=0 + if [ "$DFS_RADAR_EVENTS" -gt 0 ] || [ "$DFS_CURRENT_CONNECTION" = true ]; then + echo -e "${MAGENTA}📡 DFS-RELATED ISSUE ANALYSIS${NC}" + echo "" + + if [ "$DFS_RADAR_EVENTS" -gt 0 ]; then + echo -e "${RED}🚨 RADAR EVENTS DETECTED: $DFS_RADAR_EVENTS in last 24 hours${NC}" + echo "📋 Recent DFS/radar events:" + journalctl --since "6 hours ago" --no-pager 2>/dev/null | \ + grep -iE "radar.*detect|dfs.*radar|channel.*blocked|cac.*complete|cac.*failed" | \ + tail -5 | while IFS= read -r line; do + echo " $line" + done + DFS_LOG_ISSUES=1 + fi + + if [ "$DFS_CURRENT_CONNECTION" = true ]; then + echo -e "${YELLOW}⚠️ DFS CHANNEL CONNECTION: High risk of radar-related disconnections${NC}" + echo " Current connection uses DFS channel - this explains intermittent drops" + DFS_LOG_ISSUES=1 + fi + echo "" + fi + + # Focus on problems if WiFi is broken OR authentication issues found OR DFS issues + if [ "$WIFI_FUNCTIONAL" != "true" ] || [ "$AUTH_ISSUES" -eq 1 ] || [ "$DFS_LOG_ISSUES" -eq 1 ]; then + echo -e "${RED}💥 WiFi SYSTEM ISSUES DETECTED - DIAGNOSTIC MODE${NC}" + echo "" + + # DFS-specific fixes get priority if DFS issues detected + if [ "$DFS_LOG_ISSUES" -eq 1 ]; then + echo -e "${MAGENTA}🎯 PRIORITY: DFS FIXES (Radar interference detected)${NC}" + echo "" + echo "1. Immediate non-DFS channel switch:" + if [ -n "$CURRENT_SSID" ]; then + echo " 🔧 Force 2.4GHz (no DFS): sudo nmcli connection modify \"$CURRENT_SSID\" 802-11-wireless.band bg" + echo " 🔧 Reconnect: sudo nmcli connection up \"$CURRENT_SSID\"" + fi + echo "" + echo "2. Router configuration (CRITICAL):" + echo " 🔧 Set router primary channel to: 36, 40, 44, 48 (low 5GHz, no DFS)" + echo " 🔧 Alternative: 149, 153, 157, 161, 165 (high 5GHz, no DFS)" + echo " 🔧 Disable automatic channel selection" + echo " 🔧 Disable DFS channels entirely in router settings" + echo "" + echo "3. Test connectivity after DFS fix:" + echo " 🧪 Monitor: ping -c 10 8.8.8.8" + echo " 🧪 Verify channel: iw dev $IFACE link | grep freq" + echo "" + fi + + detect_severe_wifi_issues + + # NEW: Authentication-specific fixes - ONLY if WiFi is actually broken +if [ "$AUTH_ISSUES" -eq 1 ]; then + if [ "$WIFI_FUNCTIONAL" = "true" ]; then + echo "" + echo -e "${GREEN}✅ HISTORICAL AUTHENTICATION EVENTS (System Currently Working)${NC}" + echo "" + echo "📋 Found $AUTH_FAILURES authentication entries in logs, but:" + echo " • WiFi is currently connected and functional" + echo " • Data flow is working properly" + echo " • These appear to be historical events (likely Tailscale/boot-time handshakes)" + echo "" + echo "💡 No immediate action required - monitor for actual disconnection issues" + echo "" + else + echo "" + echo -e "${YELLOW}🔧 AUTHENTICATION FAILURE FIXES:${NC}" + echo "" + echo "1. Delete and recreate connection profile:" + echo " 🔒 PERMANENT: sudo nmcli connection delete \"$CURRENT_SSID\"" + echo " 🔒 PERMANENT: sudo nmcli device wifi connect \"$CURRENT_SSID\" password \"PASSWORD\"" + echo "" + echo "2. Force specific band to avoid problematic radios:" + echo " 🔒 PERMANENT (5GHz only): sudo nmcli connection modify \"$CURRENT_SSID\" 802-11-wireless.band a" + echo " 🔒 PERMANENT (2.4GHz only): sudo nmcli connection modify \"$CURRENT_SSID\" 802-11-wireless.band bg" + echo "" + echo "3. Reset regulatory domain (6GHz issues):" + echo " ⏰ TEMPORARY: sudo iw reg set US" + echo " ⏰ TEMPORARY: sudo nmcli radio wifi off && sudo nmcli radio wifi on" + echo "" + echo "4. Router-specific fixes:" + echo " 💡 Disable band steering in router settings" + echo " 💡 Use non-DFS channels (36,40,44,149,153,157,161,165)" + echo " 💡 For Eero mesh: Check for firmware updates in Eero app" + echo "" + fi +fi + + echo "" + echo -e "${YELLOW}🔧 DISTRIBUTION-SPECIFIC TROUBLESHOOTING STEPS:${NC}" + echo "" + echo "🐧 Detected system: $DISTRO_NAME" + echo "" + echo "1. Basic restart sequence:" + echo " ⏰ IMMEDIATE (temporary fix): sudo systemctl restart NetworkManager" + echo " ⏰ IMMEDIATE (driver reload): sudo modprobe -r $DRIVER && sleep 3 && sudo modprobe $DRIVER" + echo "" + echo "2. Update system and firmware:" + echo " 🔒 PERMANENT: $(get_distro_command "firmware_update")" + echo "" + echo "3. Check for hardware issues:" + echo " 🧪 DIAGNOSTIC: lspci | grep -i wireless" + echo " 🧪 DIAGNOSTIC: dmesg | grep -i $DRIVER | tail -20" + echo "" + echo "4. Network configuration reset:" + echo " ⏰ IMMEDIATE: sudo nmcli device disconnect $IFACE" + echo " ⏰ IMMEDIATE: sudo nmcli device connect $IFACE" + echo "" + + # Provide distribution-specific advanced fixes + provide_distribution_specific_workarounds "DRIVER_ERROR" + + echo "📁 Detailed logs can be found in system journal" + + else + echo -e "${GREEN}✅ WiFi WORKING - HEALTH CHECK MODE${NC}" + echo "" + + # Even working systems can have underlying issues + echo "🔍 Checking for potential issues in working system..." + + # Enhanced error filtering (remove harmless firmware loading attempts) + RECENT_ERRORS=$(journalctl --since "24 hours ago" --no-pager 2>/dev/null | \ + grep -iE "$DRIVER.*error|wifi.*error|$IFACE.*error" | \ + grep -v "Direct firmware load.*failed with error -2" | \ + grep -v "firmware load.*failed.*error -2" | \ + grep -v "WIFI_RAM_CODE.*failed" | \ + grep -v "WIFI_MT.*patch.*failed" | \ + grep -v "mediatek.*bin failed" | \ + grep -v "firmware.*failed.*error -2" | \ + wc -l) + + if [ "$RECENT_ERRORS" -gt 0 ]; then + echo "⚠️ Found $RECENT_ERRORS recent WiFi-related errors" + echo "📋 Recent error patterns:" + journalctl --since "24 hours ago" --no-pager 2>/dev/null | \ + grep -iE "$DRIVER.*error|wifi.*error" | \ + grep -v "firmware.*failed.*error -2" | \ + tail -5 | while IFS= read -r line; do + echo " $line" + done + else + echo "✅ No recent WiFi errors detected" + fi + + # Modern VPN conflict check + if [ "$VPN_ACTIVE" = "Yes" ] && [ "$VPN_IMPACT_SCORE" -gt 30 ]; then + echo "" + echo "⚠️ Modern VPN may be impacting WiFi performance" + echo "💡 Consider testing WiFi without VPN periodically" + fi + + # DFS health check even for working systems + if [ "$DFS_IMPACT_SCORE" -gt 25 ]; then + echo "" + echo "📡 DFS Analysis: Potential radar interference risk detected" + echo "💡 Monitor for disconnection patterns, especially 30+ second drops" + if [ "$DFS_CURRENT_CONNECTION" = true ]; then + echo "⚠️ Current connection uses DFS channel - consider switching" + fi + fi + + echo "" + echo "🎉 System appears healthy - no immediate action required" + fi + + echo "" +} + +# Interactive workaround generator with modern solutions and DFS fixes - COMPLETE +interactive_workaround_generator() { + echo -e "${BOLD}${BLUE}🛠️ === INTERACTIVE WORKAROUND GENERATOR ===${NC}" + echo "🎯 Modern solutions for WiFi 7, 6GHz, MLO, DFS radar interference, and advanced VPN issues" + echo "" + + echo "What WiFi issue are you experiencing?" + echo "" + echo "1) 📡 WiFi keeps disconnecting/dropping" + echo "2) 🐌 WiFi is very slow or unstable" + echo "3) ❌ WiFi won't connect at all" + echo "4) 🔒 Modern VPN causes WiFi problems (Tailscale/ZeroTier/etc)" + echo "5) 🔥 WiFi works but system gets hot/fans spin" + echo "6) ⚡ WiFi stops working after suspend/resume" + echo "7) 📶 Weak signal or poor range" + echo "8) 📡 DFS radar interference (sudden 30+ second disconnections)" + echo "9) 🆘 Emergency: Need immediate solution" + echo "10) 🔍 Run diagnostic first (recommended)" + echo "" + echo -n "Select your issue [1-10]: " + + read -r issue_choice + + case $issue_choice in + 1) + echo "" + echo "🔍 Analyzing modern disconnection patterns..." + # Re-run analysis and UPDATE global variables + gather_system_intelligence >/dev/null 2>&1 + detect_vpn_configuration >/dev/null 2>&1 + analyze_dfs_channels >/dev/null 2>&1 + + # Check if WiFi is actually having disconnection issues + if [ "$WIFI_FUNCTIONAL" = "true" ] && [ "$DFS_CURRENT_CONNECTION" != "true" ] && [ "$DFS_RADAR_EVENTS" -eq 0 ]; then + echo -e "${GREEN}✅ === WIFI CURRENTLY STABLE ===${NC}" + echo "" + echo "🎉 Analysis shows your WiFi is working excellently:" + echo " • Connected to 6GHz at high speeds" + echo " • No DFS radar interference" + echo " • No current disconnection issues detected" + echo "" + echo "❓ Are you experiencing actual disconnections right now? [y/N]" + read -r experiencing_issues + + if [[ ! "$experiencing_issues" =~ ^[Yy]$ ]]; then + echo "" + echo "💡 Your WiFi appears stable. Consider monitoring with:" + echo " • Real-time connection: watch -n 2 'iw dev $IFACE link | grep -E \"Connected|signal\"'" + echo " • Network stability: ping -i 1 8.8.8.8" + echo "" + echo "💡 If disconnections occur later, re-run this tool for targeted fixes." + return 0 + fi + echo "" + echo "🔍 Proceeding with disconnection analysis since you're experiencing issues..." + fi + + echo -e "${CYAN}🛠️ === MODERN DISCONNECTION FIXES ===${NC}" + echo "" + echo "🐧 Distribution: $DISTRO_NAME" + echo "" + + # DFS gets priority for disconnection issues + if [ "$DFS_CURRENT_CONNECTION" = true ] || [ "$DFS_RADAR_EVENTS" -gt 0 ]; then + echo -e "${MAGENTA}🎯 PRIORITY: DFS Radar Interference (likely cause)${NC}" + echo "" + echo "1. Immediate non-DFS channel switch:" + if [ -n "$CURRENT_SSID" ]; then + echo " ⏰ IMMEDIATE: sudo nmcli connection modify \"$CURRENT_SSID\" 802-11-wireless.band bg" + echo " ⏰ IMMEDIATE: sudo nmcli connection up \"$CURRENT_SSID\"" + echo " 💡 This forces 2.4GHz (no DFS channels exist in 2.4GHz)" + fi + echo "" + echo "2. Router configuration (CRITICAL):" + echo " 🔒 PERMANENT: Set router to channels 36, 40, 44, 48 (low 5GHz, no DFS)" + echo " 🔒 PERMANENT: Alternative: 149, 153, 157, 161, 165 (high 5GHz, no DFS)" + echo " 🔒 PERMANENT: Disable automatic channel selection" + echo "" + echo "3. Test after DFS fix:" + echo " 🧪 Monitor: ping -c 20 8.8.8.8 # Should show no 30+ second gaps" + echo " 🧪 Verify: iw dev $IFACE link | grep freq # Confirm non-DFS frequency" + echo "" + echo "4. 6GHz upgrade path (NO DFS):" + if [ "$SUPPORTS_WIFI7" = true ]; then + echo " ✅ Your hardware supports 6GHz" + echo " 🔒 PERMANENT: Upgrade to WiFi 6E/7 router for DFS-free operation" + echo " 🌟 6GHz advantage: Zero radar interference possible" + fi + echo "" + elif [ "$WIFI_FUNCTIONAL" != "true" ]; then + echo "1. Modern driver reset:" + echo " ⏰ IMMEDIATE (driver reload): sudo modprobe -r $DRIVER && sleep 5 && sudo modprobe $DRIVER" + echo "" + echo "2. Distribution-specific firmware update:" + echo " 🔒 PERMANENT (system upgrade): $(get_distro_command "firmware_update")" + echo "" + provide_distribution_specific_workarounds "FIRMWARE_CRASH" + elif [ "$VPN_ACTIVE" = "Yes" ] && [ "$VPN_IMPACT_SCORE" -gt 30 ]; then + echo "🔒 Modern VPN optimization detected:" + echo "" + echo "1. Optimize modern VPN MTU:" + echo " ⏰ TEMPORARY (until VPN restart): sudo ip link set $VPN_INTERFACE mtu 1200" + echo " 🔒 PERMANENT: Configure in VPN client settings" + echo "" + echo "2. For Tailscale specifically:" + echo " 🔒 PERMANENT (Tailscale config): tailscale up --accept-routes=false" + echo " 💡 Enables split tunneling - setting persists" + echo "" + echo "3. For ZeroTier:" + echo " 💡 Check ZeroTier controller settings for route conflicts" + echo " 🔒 PERMANENT: Configure managed routes in ZeroTier Central" + echo "" + echo "4. Test without VPN:" + echo " 🧪 TESTING (diagnostic): Temporarily disconnect modern VPN to test WiFi stability" + echo " ⏰ TEMPORARY: Changes revert when VPN reconnects" + else + echo "1. Modern power management fix:" + echo " ⏰ TEMPORARY (until reboot): sudo iw dev $IFACE set power_save off" + echo " 🔒 PERMANENT (module config): echo 'options mt7921e power_save=0' | sudo tee /etc/modprobe.d/mt7921e.conf" + echo " 💡 Try temporary first to test, then apply permanent if it works" + echo "" + echo "3. WiFi 7/6E specific optimizations:" + if [ "$SUPPORTS_WIFI7" = true ]; then + echo " 🔒 PERMANENT (system upgrade): $(get_distro_command "firmware_update")" + echo " ⚠️ REQUIRES REBOOT: System update needs restart to take effect" + echo " 💡 Your WiFi 7 hardware may need newer firmware for stability" + else + echo " 💡 Standard WiFi 6E optimizations" + echo " 💡 Consider hardware upgrade for WiFi 7 features" + fi + echo "" + echo "4. Distribution-specific module tuning:" + provide_distribution_specific_workarounds "DRIVER_ERROR" + fi + ;; + 2) + echo "" + echo "🔍 Analyzing modern performance issues..." + gather_system_intelligence >/dev/null 2>&1 + analyze_rf_frequency_environment >/dev/null 2>&1 + + # Check if system is already performing excellently + if [ "$WIFI_FUNCTIONAL" = "true" ] && [ "$CURRENT_BAND" = "6 GHz (WiFi 6E/7)" ] && [ -n "$CURRENT_BITRATE" ]; then + BITRATE_NUM=$(echo "$CURRENT_BITRATE" | grep -o "[0-9]*" | head -1) + if [ -n "$BITRATE_NUM" ] && [ "$BITRATE_NUM" -gt 1000 ]; then + echo -e "${GREEN}✅ === EXCELLENT PERFORMANCE DETECTED ===${NC}" + echo "" + echo "🚀 Your system shows outstanding performance metrics:" + echo " • Connected to 6GHz clean spectrum" + echo " • Speed: ${CURRENT_BITRATE:-Unknown} Mbps" + echo " • Signal: ${CURRENT_SIGNAL:-Unknown} dBm" + echo " • Channel width: ${CHANNEL_WIDTH:-Unknown} MHz" + echo "" + echo "❓ Are you experiencing actual speed/performance issues? [y/N]" + read -r experiencing_performance_issues + + if [[ ! "$experiencing_performance_issues" =~ ^[Yy]$ ]]; then + echo "" + echo "💡 Your WiFi is performing excellently. For monitoring:" + echo " • Speed test: speedtest-cli" + echo " • Real-time stats: watch -n 2 'iw dev $IFACE link'" + echo " • Advanced metrics: iperf3 -c your-server-ip" + echo "" + echo "🎯 Possible optimizations for your excellent setup:" + echo " • Router upgrade to WiFi 7 for 320MHz channels" + echo " • MLO (Multi-Link Operation) if router supports it" + echo "" + return 0 + fi + echo "" + echo "🔍 Proceeding with performance optimization since you're experiencing issues..." + fi + fi + + echo -e "${CYAN}🛠️ === MODERN PERFORMANCE OPTIMIZATION ===${NC}" + echo "" + + # DFS impact on performance + if [ "$DFS_CURRENT_CONNECTION" = true ]; then + echo -e "${MAGENTA}📡 DFS PERFORMANCE IMPACT DETECTED${NC}" + echo "" + echo " Current DFS connection may cause:" + echo " • Sudden speed drops during radar detection" + echo " • 30+ second interruptions for channel switching" + echo " • Inconsistent throughput patterns" + echo "" + echo " 🎯 Fix: Switch to non-DFS channel for consistent performance" + if [ -n "$CURRENT_SSID" ]; then + echo " ⏰ IMMEDIATE: sudo nmcli connection modify \"$CURRENT_SSID\" 802-11-wireless.band bg" + fi + echo "" + fi + + echo "1. Modern band optimization:" + if [ "$CURRENT_BAND" = "2.4 GHz" ]; then + echo " ⏰ TEMPORARY (test only): Force 5GHz connection" + echo " 🔒 PERMANENT: sudo nmcli connection modify \"$CURRENT_SSID\" 802-11-wireless.band a" + echo " 💡 Better: Upgrade to WiFi 6E/7 router for 6GHz access" + elif [ "$CURRENT_BAND" = "5 GHz" ]; then + echo " 📊 You're on 5GHz - optimization options:" + echo " 💡 Consider WiFi 6E/7 router upgrade for 6GHz clean spectrum" + if [ "$SUPPORTS_WIFI7" = true ]; then + echo " 💡 Your hardware supports WiFi 7 - router upgrade recommended" + fi + if [ "$DFS_CURRENT_CONNECTION" = true ]; then + echo " ⚠️ Current DFS channel may impact performance consistency" + fi + elif [ "$CURRENT_BAND" = "6 GHz (WiFi 6E/7)" ]; then + echo " ✅ Excellent! You're on 6GHz clean spectrum" + echo " 🌟 6GHz advantage: No DFS = consistent performance" + echo " 💡 Optimize channel width and MLO if available" + fi + echo "" + + echo "2. Modern channel width optimization:" + if [ -n "$CHANNEL_WIDTH" ]; then + echo " 📊 Current: $CHANNEL_WIDTH MHz" + if [ "$CHANNEL_WIDTH" -lt 80 ]; then + echo " 🔒 PERMANENT (router config): Increase to 80MHz or 160MHz in router settings" + elif [ "$CHANNEL_WIDTH" -eq 160 ] && [ "$SUPPORTS_WIFI7" = true ]; then + echo " 🔒 PERMANENT (router upgrade): Consider 320MHz if router supports WiFi 7" + fi + fi + echo "" + + echo "3. MLO optimization (WiFi 7):" + if [ "$SUPPORTS_MLO" = true ]; then + echo " 🔒 PERMANENT (router config): Enable MLO in router settings for multi-band aggregation" + echo " ⚠️ REQUIRES: WiFi 7 router with MLO support" + else + echo " 💡 MLO not supported - consider WiFi 7 hardware upgrade" + fi + echo "" + ;; + 3) + echo "" + echo "🔍 Analyzing modern connection failures..." + + # Re-run analysis to get current status + gather_system_intelligence >/dev/null 2>&1 + + # Check if WiFi is actually connected and working +if [ "$WIFI_FUNCTIONAL" = "true" ]; then + # Try to get current SSID if it's missing + if [ -z "$CURRENT_SSID" ]; then + CURRENT_SSID=$(nmcli -t -f active,ssid dev wifi | grep '^yes' | cut -d: -f2 2>/dev/null) + if [ -z "$CURRENT_SSID" ]; then + CURRENT_SSID=$(iw dev "$IFACE" link 2>/dev/null | grep "SSID:" | awk '{print $2}') + fi + fi + + echo -e "${GREEN}✅ === WIFI CURRENTLY CONNECTED ===${NC}" + echo "" + echo "🎉 Your WiFi appears to be working:" + echo " • Connected to: ${CURRENT_SSID:-"(Connected but SSID detection failed)"}" + echo " • Band: ${CURRENT_BAND:-Unknown}" + echo " • Speed: ${CURRENT_BITRATE:-Unknown} Mbps" + echo " • Signal: ${CURRENT_SIGNAL:-Unknown} dBm" + echo "" + echo "❓ Are you experiencing actual connection failures? [y/N]" + read -r experiencing_connection_issues + + if [[ ! "$experiencing_connection_issues" =~ ^[Yy]$ ]]; then + echo "" + echo "💡 Your WiFi is connected and working. For troubleshooting:" + echo " • Check other devices: Test if issue affects other devices" + echo " • Monitor connection: watch -n 2 'iw dev $IFACE link'" + echo " • Test specific sites: curl -I google.com" + echo "" + return 0 + fi + echo "" + echo "🔍 Proceeding with connection troubleshooting since you're experiencing issues..." +fi + + echo -e "${CYAN}🛠️ === MODERN CONNECTION FIXES ===${NC}" + echo "" + echo "🐧 System: $DISTRO_NAME" + echo "" + + echo "1. Emergency connection reset:" + echo " ⏰ IMMEDIATE: sudo systemctl restart NetworkManager" + echo " ⏰ IMMEDIATE: sudo modprobe -r $DRIVER && sudo modprobe $DRIVER" + echo "" + + provide_distribution_specific_workarounds "DRIVER_ERROR" + ;; + 4) + echo "" + echo "🔍 Analyzing modern VPN conflicts..." + detect_vpn_configuration >/dev/null 2>&1 + + echo -e "${CYAN}🛠️ === MODERN VPN OPTIMIZATION ===${NC}" + echo "" + + if echo "$VPN_TYPE" | grep -qi "tailscale"; then + echo "🔗 Tailscale (WireGuard mesh) optimization:" + echo "" + echo "1. Enable split tunneling:" + echo " 🔒 PERMANENT: tailscale up --accept-routes=false" + echo "" + echo "2. Optimize MTU for modern networks:" + echo " ⏰ TEMPORARY: sudo ip link set tailscale0 mtu 1200" + echo "" + echo "3. Use modern exit nodes efficiently:" + echo " 🔒 PERMANENT: tailscale up --exit-node=COUNTRY-CODE" + echo "" + elif echo "$VPN_TYPE" | grep -qi "zerotier"; then + echo "🌐 ZeroTier (SD-WAN) optimization:" + echo "" + echo "1. Check controller settings for route conflicts" + echo "2. ⏰ TEMPORARY: sudo ip link set zt+ mtu 1200" + echo "3. 🔒 PERMANENT: Use managed routes instead of full routing" + echo "" + else + echo "🔒 Generic modern VPN optimization:" + echo "" + echo "1. MTU optimization:" + echo " ⏰ TEMPORARY: sudo ip link set $VPN_INTERFACE mtu 1200" + echo "" + echo "2. 🔒 PERMANENT: Split tunneling configuration" + echo "3. 🔒 PERMANENT: Modern DNS configuration" + fi + ;; + 5) + echo "" + echo "🔍 Analyzing thermal/power issues..." + # Get current system info + gather_system_intelligence >/dev/null 2>&1 + + echo -e "${CYAN}🛠️ === ADVANCED THERMAL MANAGEMENT ===${NC}" + echo "" + + echo "🌡️ IMMEDIATE THERMAL FIXES:" + echo "" + echo "1. WiFi power management optimization:" + echo " ⏰ IMMEDIATE: sudo iw dev $IFACE set power_save off" + echo " 🔒 PERMANENT: echo 'options $DRIVER power_save=0' | sudo tee /etc/modprobe.d/$DRIVER.conf" + echo " ⚠️ REQUIRES REBOOT after permanent config" + echo "" + + echo "2. ASPM (Advanced State Power Management) fixes:" + if echo "$CHIP_MODEL" | grep -qi "mt79"; then + echo " MediaTek-specific thermal optimization:" + echo " 🔒 PERMANENT: echo 'options $DRIVER disable_aspm=1' | sudo tee -a /etc/modprobe.d/$DRIVER.conf" + echo " 🔒 PERMANENT: $(get_distro_command "kernel_param")'pcie_aspm=off'" + echo " ⚠️ REQUIRES REBOOT and GRUB update" + else + echo " Generic ASPM optimization:" + echo " 🔒 PERMANENT: $(get_distro_command "kernel_param")'pcie_aspm=off'" + echo " ⚠️ REQUIRES REBOOT" + fi + echo "" + + echo "3. CPU governor optimization for thermal control:" +echo " ⏰ IMMEDIATE: for cpu in /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor; do echo powersave | sudo tee \"\$cpu\"; done" +echo " 🔒 PERMANENT: Use built-in system power management" + +# Distribution-specific thermal management +case "$DISTRO_ID" in + "fedora"|"rhel"|"centos"|"rocky"|"almalinux") + echo " Fedora/RHEL: sudo tuned-adm profile powersave" + echo " Alternative: sudo systemctl enable power-profiles-daemon" + ;; + "ubuntu"|"debian"|"pop"|"mint"|"linuxmint") + echo " Ubuntu/Debian: sudo systemctl enable power-profiles-daemon" + echo " Alternative: sudo cpupower frequency-set -g powersave" + ;; + "arch"|"manjaro"|"endeavouros") + echo " Arch: sudo systemctl enable power-profiles-daemon" + echo " Alternative: echo 'powersave' | sudo tee /etc/default/cpufrequtils" + ;; + *) + echo " Generic: Use your distribution's built-in power management" + ;; +esac +echo "" + + echo "4. Modern WiFi 7 thermal considerations:" + if [ "$SUPPORTS_WIFI7" = true ]; then + echo " WiFi 7 hardware detected - higher power consumption expected" + echo " 🔧 Monitor temperatures: watch -n 2 'sensors | grep -E \"Core|Package|wifi\"'" + echo " 🔧 Check if 6GHz radio can be disabled when not needed" + echo " 💡 Some WiFi 7 cards run significantly hotter than WiFi 6" + fi + echo "" + + echo "5. Physical thermal optimization:" + echo " 🔧 Laptop: Ensure proper ventilation, clean fans/vents" + echo " 🔧 Desktop: Verify case airflow, WiFi card positioning" + echo " 🔧 M.2 cards: Consider thermal pads or heatsinks for hot-running cards" + echo "" + + echo "6. Firmware thermal optimization:" + echo " 🔒 PERMANENT: $(get_distro_command "firmware_update")" + echo " 💡 Newer firmware often includes thermal improvements" + echo " ⚠️ REQUIRES REBOOT after firmware update" + echo "" + + echo "🧪 THERMAL MONITORING COMMANDS:" + echo " 📊 CPU temps: watch -n 2 'sensors | grep Core'" + echo " 📊 All temps: sudo watch -n 2 'sensors'" + echo " 📊 WiFi power: watch -n 5 'iw dev $IFACE info | grep txpower'" + echo " 📊 System load: watch -n 2 'uptime && cat /proc/loadavg'" + ;; + 6) + echo "" + echo "🔍 Analyzing modern suspend/resume issues..." + # Re-run analysis to get current system info + gather_system_intelligence >/dev/null 2>&1 + analyze_modern_chipsets >/dev/null 2>&1 + + echo -e "${CYAN}🛠️ === MODERN SUSPEND/RESUME FIXES ===${NC}" +echo "" + +echo "1. Complete systemd sleep script (recommended):" +echo " 🔒 PERMANENT: sudo tee /etc/systemd/system-sleep/wifi-resume.sh << 'EOF'" +echo "#!/bin/bash" +echo "if [ \"\$1\" = \"post\" ]; then" +echo " modprobe -r $DRIVER" +echo " sleep 2" +echo " modprobe $DRIVER" +if [ "$SUPPORTS_WIFI7" = true ]; then + echo " # WiFi 7: Allow 6GHz initialization" + echo " sleep 3" +fi +echo " # Reset power management and restart NetworkManager" +echo " sleep 1" +echo " iw dev $IFACE set power_save off 2>/dev/null || true" +echo " systemctl restart NetworkManager" +echo "fi" +echo "EOF" +echo " sudo chmod +x /etc/systemd/system-sleep/wifi-resume.sh" +echo "" + +echo "2. Lightweight alternative (NetworkManager only):" +echo " 🔒 PERMANENT: sudo tee /etc/systemd/system-sleep/nm-restart.sh << 'EOF'" +echo "#!/bin/bash" +echo "[ \"\$1\" = \"post\" ] && systemctl restart NetworkManager" +echo "EOF" +echo " sudo chmod +x /etc/systemd/system-sleep/nm-restart.sh" +echo "" + +echo "3. Test suspend/resume fix:" +echo " 🧪 TEST: sudo systemctl suspend" +echo " 🧪 VERIFY: After resume - iw dev $IFACE link" +echo "" +;; + 7) + echo "" + echo "🔍 Analyzing modern signal optimization..." + # Re-run analysis to get current system info + gather_system_intelligence >/dev/null 2>&1 + analyze_rf_frequency_environment >/dev/null 2>&1 + + echo -e "${CYAN}🛠️ === ADVANCED SIGNAL OPTIMIZATION ===${NC}" + echo "" + + echo "📊 Current Signal Analysis:" + echo " Signal Strength: ${CURRENT_SIGNAL:-Unknown} dBm" + echo " Current Band: ${CURRENT_BAND:-Unknown}" + echo " Current Frequency: ${CURRENT_FREQ:-Unknown} MHz" + echo "" + + echo "🚀 PROVEN SIGNAL IMPROVEMENT TECHNIQUES:" + echo "" + + echo "1. Band optimization for maximum range:" + echo " 💡 2.4GHz: Better penetration through walls, longer range" + echo " 💡 5GHz: Less congested, shorter range but higher speeds" + echo " 💡 6GHz: Cleanest spectrum, shortest range, highest speeds" + if [ -n "$CURRENT_SSID" ]; then + echo "" + echo " Switch commands for your network:" + echo " ⏰ IMMEDIATE (2.4GHz): sudo nmcli connection modify \"$CURRENT_SSID\" 802-11-wireless.band bg" + echo " ⏰ IMMEDIATE (5GHz): sudo nmcli connection modify \"$CURRENT_SSID\" 802-11-wireless.band a" + echo " 💡 Test each band to find optimal signal/speed balance" + fi + echo "" + + echo "2. Antenna diversity and positioning:" + echo " 🔧 Laptop: Adjust screen angle (affects internal antenna orientation)" + echo " 🔧 Desktop: Ensure M.2 WiFi card antennas are properly connected" + echo " 🔧 USB adapters: Try different USB ports, use extension cable" + echo " 🔧 External antennas: Position perpendicular to router antennas" + echo "" + + echo "3. Modern regulatory domain optimization:" + echo " 📡 Current domain: $(iw reg get | grep country || echo 'Not set')" + echo " 🔧 Optimize for your location:" + echo " ⏰ IMMEDIATE: sudo iw reg set US # (or your country code)" + echo " 💡 Proper regulatory domain can increase allowed TX power" + echo "" + + echo "4. Channel width optimization for range vs speed:" + if [ -n "$CHANNEL_WIDTH" ]; then + echo " 📊 Current width: $CHANNEL_WIDTH MHz" + if [ "$CHANNEL_WIDTH" -ge 80 ]; then + echo " 💡 Wide channels give speed but reduce range" + echo " 🔧 For better range: Force router to 40MHz or 80MHz" + fi + fi + echo " 💡 Wider channels = faster speeds but shorter range" + echo " 💡 Narrower channels = longer range but slower speeds" + echo "" + + # Chipset-specific TX power advice + if echo "$CHIP_MODEL" | grep -qi "mt79"; then + echo " 📝 MediaTek note: TX power commands often fail or are ignored" + echo " 💡 Router-side power increase usually more effective" + elif echo "$CHIP_MODEL" | grep -qi "intel"; then + echo " 📝 Intel note: Regulatory restrictions often limit manual TX power" + echo " 💡 Ensure proper regulatory domain is set first" + fi + echo "" + + echo "6. Physical optimization techniques:" + echo " 🏠 Router placement: Central location, elevated position" + echo " 🏠 Reduce obstacles: Minimize walls, large objects between devices" + echo " 🏠 Interference reduction: Keep away from microwaves, baby monitors" + echo " 📱 Client positioning: Higher floors often get better signal" + echo "" + + echo "7. WiFi 6E/7 specific optimizations:" + if [ "$SUPPORTS_WIFI7" = true ]; then + echo " ✅ Your hardware supports modern WiFi features" + echo " 🌟 6GHz band: Clean spectrum but limited range" + echo " 🔧 Use 6GHz for close-range, high-speed connections" + echo " 🔧 Use 5GHz for medium-range connections" + echo " 🔧 Use 2.4GHz for maximum range connections" + + if [ "$SUPPORTS_MLO" = true ]; then + echo " 🚀 MLO capable: Can use multiple bands simultaneously" + echo " 💡 Requires MLO-capable router for maximum benefit" + fi + else + echo " 💡 Current hardware: WiFi 6 or older" + echo " 💡 Consider WiFi 6E/7 upgrade for access to 6GHz clean spectrum" + fi + echo "" + + echo "8. Real-time signal monitoring:" + echo " 📊 Watch signal: watch -n 1 'iw dev $IFACE link | grep signal'" + echo " 📊 Site survey: sudo iw dev $IFACE scan | grep -E 'SSID|signal|freq'" + echo " 📊 Speed test: speedtest-cli (install if needed)" + echo "" + + echo "🎯 TESTING PROTOCOL:" + echo "1. Baseline test: speedtest-cli && iw dev $IFACE link | grep signal" + echo "2. Try 2.4GHz: sudo nmcli connection modify \"$CURRENT_SSID\" 802-11-wireless.band bg" + echo "3. Test again: speedtest-cli && iw dev $IFACE link | grep signal" + echo "4. Try 5GHz: sudo nmcli connection modify \"$CURRENT_SSID\" 802-11-wireless.band a" + echo "5. Test again: speedtest-cli && iw dev $IFACE link | grep signal" + echo "6. Use band with best signal/speed ratio for your location" + ;; + 8) + echo "" + echo "🔍 Analyzing DFS radar interference patterns..." + gather_system_intelligence >/dev/null 2>&1 + analyze_dfs_channels >/dev/null 2>&1 + + echo -e "${MAGENTA}🛠️ === DFS RADAR INTERFERENCE FIXES ===${NC}" + echo "" + + echo "📡 DFS Analysis Results:" + echo " Current DFS connection: $([ "$DFS_CURRENT_CONNECTION" = true ] && echo "Yes (HIGH RISK)" || echo "No")" + echo " DFS networks in area: $DFS_COUNT" + echo " Recent radar events: $DFS_RADAR_EVENTS" + echo " DFS risk score: $DFS_IMPACT_SCORE/100" + echo "" + + echo "🎯 IMMEDIATE DFS FIXES:" + echo "" + echo "1. Emergency non-DFS channel switch:" + if [ -n "$CURRENT_SSID" ]; then + echo " ⏰ IMMEDIATE: sudo nmcli connection modify \"$CURRENT_SSID\" 802-11-wireless.band bg" + echo " ⏰ IMMEDIATE: sudo nmcli connection up \"$CURRENT_SSID\"" + echo " 💡 Forces 2.4GHz connection - no DFS channels in 2.4GHz band" + fi + echo "" + echo "2. Router configuration (CRITICAL - prevents future issues):" + echo " 🔒 PERMANENT: Primary 5GHz channel → 36, 40, 44, or 48 (low band, no DFS)" + echo " 🔒 PERMANENT: Alternative channels → 149, 153, 157, 161, 165 (high band, no DFS)" + echo " 🔒 PERMANENT: Disable automatic channel selection" + echo " 🔒 PERMANENT: Disable DFS channels entirely (if router supports this option)" + echo "" + echo "3. Verification and testing:" + echo " 🧪 Test connectivity: ping -c 30 8.8.8.8" + echo " 💡 No timeouts should occur (previously had 30+ second gaps)" + echo " 🧪 Verify channel: iw dev $IFACE link | grep freq" + echo " 💡 Frequency should NOT be 5260-5320 MHz or 5500-5700 MHz (DFS ranges)" + echo "" + echo "4. Long-term DFS-free solutions:" + echo "" + echo " Option A - 6GHz Migration (BEST):" + if [ "$SUPPORTS_WIFI7" = true ]; then + echo " ✅ Your hardware supports 6GHz" + echo " 🔒 PERMANENT: Upgrade to WiFi 6E/7 router" + echo " 🌟 6GHz band: ALL channels are DFS-free (no radar interference possible)" + echo " 🚀 Additional benefits: Clean spectrum, higher throughput, lower latency" + else + echo " 💡 Hardware upgrade to WiFi 6E/7 required" + echo " 💡 6GHz band: ALL channels are DFS-free" + echo " 💡 Recommended cards: MT7925, Intel BE200, Qualcomm WCN7850" + fi + echo "" + echo " Option B - Strategic 5GHz Channel Planning:" + echo " 🔒 PERMANENT: Use ONLY these channels: 36, 40, 44, 48, 149, 153, 157, 161, 165" + echo " 💡 These channels never require DFS and are immune to radar" + echo " 💡 Avoid channels 52-64 and 100-144 (all DFS channels)" + echo "" + + # Provide DFS-specific monitoring commands + echo "🔍 DFS Monitoring Commands:" + echo "" + echo "Real-time radar event monitoring:" + echo " 📋 journalctl -f | grep -iE 'radar|dfs|cac'" + echo "" + echo "Channel stability monitoring:" + echo " 📋 watch -n 2 'iw dev $IFACE link | grep freq'" + echo "" + echo "Connection stability test:" + echo " 📋 ping -i 1 8.8.8.8 | ts" + echo " 💡 Look for gaps > 30 seconds (indicates radar detection)" + echo "" + ;; + 9) + echo "" + echo "🆘 Emergency fixes - Choose your emergency type:" + echo "" + echo "1) Complete WiFi failure (not connecting at all)" + echo "2) Frequent disconnections (working but unstable)" + echo "3) Very slow speeds (connected but poor performance)" + echo "4) Overheating system (fans spinning, hot)" + echo "" + echo -n "Emergency type [1-4]: " + read -r emergency_type + + case $emergency_type in + 1) + echo "" + echo "🚨 EMERGENCY: Complete WiFi failure fixes" + echo "" + echo "Try these in order, test after each:" + echo "" + echo "1. Restart network services:" + echo " sudo systemctl restart NetworkManager" + echo " sudo systemctl restart wpa_supplicant" + echo "" + echo "2. Reset WiFi driver:" + echo " sudo modprobe -r $DRIVER && sleep 5 && sudo modprobe $DRIVER" + echo "" + echo "3. Turn WiFi off and on:" + echo " sudo nmcli radio wifi off && sleep 5 && sudo nmcli radio wifi on" + echo "" + echo "4. Reset regulatory domain:" + echo " sudo iw reg set US # (or your country code)" + echo "" + echo "5. If nothing works - reboot:" + echo " sudo reboot" + ;; + 2) + echo "" + echo "🚨 EMERGENCY: Disconnection fixes" + echo "" + if [ "$DFS_CURRENT_CONNECTION" = true ]; then + echo "🎯 DFS detected - likely cause of disconnections!" + echo "" + echo "Emergency DFS fix:" + if [ -n "$CURRENT_SSID" ]; then + echo " sudo nmcli connection modify \"$CURRENT_SSID\" 802-11-wireless.band bg" + echo " sudo nmcli connection up \"$CURRENT_SSID\"" + fi + else + echo "Emergency stability fixes:" + echo "" + echo "1. Disable power saving:" + echo " sudo iw dev $IFACE set power_save off" + echo "" + echo "2. Force 2.4GHz (most stable):" + if [ -n "$CURRENT_SSID" ]; then + echo " sudo nmcli connection modify \"$CURRENT_SSID\" 802-11-wireless.band bg" + fi + fi + ;; + 3) + echo "" + echo "🚨 EMERGENCY: Speed improvement fixes" + echo "" + echo "1. Switch to 5GHz:" + if [ -n "$CURRENT_SSID" ]; then + echo " sudo nmcli connection modify \"$CURRENT_SSID\" 802-11-wireless.band a" + fi + echo "" + echo "2. Disable power saving:" + echo " sudo iw dev $IFACE set power_save off" + echo "" + ;; + 4) + echo "" + echo "🚨 EMERGENCY: Overheating fixes" + echo "" + echo "1. Enable power saving:" + echo " sudo iw dev $IFACE set power_save on" + echo "" + echo "2. Set CPU governor to powersave:" + echo " echo powersave | sudo tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor" + echo "" + echo "3. Force 2.4GHz (lower power):" + if [ -n "$CURRENT_SSID" ]; then + echo " sudo nmcli connection modify \"$CURRENT_SSID\" 802-11-wireless.band bg" + fi + ;; + esac + ;; + 10) + echo "" + echo "🔍 Running comprehensive diagnostic first..." + complete_wifi_analysis + echo "" + echo "Based on diagnostic results, what specific issue do you see?" + echo "Re-run this option (3) and select specific issue number." + ;; + *) + echo "" + echo "❌ Invalid option selected" + echo "💡 Please select a number between 1-10" + ;; + esac + + echo "" + echo -e "${GREEN}💡 TIP: Save these modern commands for future use!${NC}" + echo "📋 All solutions are tailored for your $DISTRO_NAME system" + if [ "$DFS_IMPACT_SCORE" -gt 25 ]; then + echo "📡 DFS considerations included for radar-free operation" + fi + echo "" +} + +# DFS-specific recommendations +provide_dfs_recommendations() { + echo -e "${CYAN}💡 === DFS OPTIMIZATION RECOMMENDATIONS ===${NC}" + echo "" + + # Sanitize variables at start of function + DFS_COUNT=$(sanitize_number "$DFS_COUNT" "0") + DFS_RADAR_EVENTS=$(sanitize_number "$DFS_RADAR_EVENTS" "0") + + if [ "$DFS_CURRENT_CONNECTION" = true ]; then + echo "🎯 IMMEDIATE ACTIONS (Currently on DFS channel):" + echo "" + echo "1. Switch to non-DFS channel:" + if [ -n "$CURRENT_SSID" ]; then + echo " 🔧 Force 2.4GHz (no DFS): sudo nmcli connection modify \"$CURRENT_SSID\" 802-11-wireless.band bg" + echo " 🔧 Force low 5GHz (36-48): Configure router to use channels 36, 40, 44, 48" + echo " 🔧 Force high 5GHz (149+): Configure router to use channels 149, 153, 157, 161, 165" + fi + echo "" + echo "2. Router configuration (PRIORITY FIX):" + echo " 🔧 Set primary 5GHz channel to: 36, 40, 44, 48 (low band, no DFS)" + echo " 🔧 Alternative channels: 149, 153, 157, 161, 165 (high band, no DFS)" + echo " 🔧 Disable automatic channel selection if using DFS channels" + echo " 🔧 Disable DFS channels entirely in router settings (if available)" + echo "" + fi + + if [ "$DFS_COUNT" -gt 5 ] || [ "$DFS_RADAR_EVENTS" -gt 0 ]; then + echo "🎯 ENVIRONMENT OPTIMIZATION (High DFS area):" + echo "" + echo "1. Preferred channel strategy:" + echo " 📡 2.4GHz: Channels 1, 6, 11 (no DFS, good for basic connectivity)" + echo " 📡 5GHz Low: Channels 36, 40, 44, 48 (no DFS, universally available)" + echo " 📡 5GHz High: Channels 149, 153, 157, 161, 165 (no DFS, less congested)" + echo "" + echo "2. Modern WiFi 6E/7 optimization:" + if [ "$SUPPORTS_WIFI7" = true ] || echo "$CHIP_MODEL" | grep -qi "6E\|mt7922\|ax210\|ax211"; then + echo " 🌟 6GHz migration: NO DFS in 6GHz band!" + echo " 🔧 Router upgrade: WiFi 6E/7 with 6GHz eliminates DFS issues" + echo " 💡 6GHz channels 1-233 are all non-DFS" + else + echo " 💡 Consider WiFi 6E/7 hardware upgrade for DFS-free 6GHz" + fi + echo "" + fi + + echo "🎯 MONITORING & PREVENTION:" + echo "" + echo "1. DFS event monitoring:" + echo " 🔍 Monitor logs: journalctl -f | grep -i 'radar\\|dfs\\|cac'" + echo " 🔍 Check current channel: watch -n 5 'iw dev $IFACE link | grep freq'" + echo "" + echo "2. Router firmware updates:" + echo " 🔧 Update router firmware (better DFS handling)" + echo " 🔧 Check for radar detection sensitivity settings" + echo "" + echo "3. Connection profile optimization:" + if [ -n "$CURRENT_SSID" ]; then + echo " 🔧 Create separate 2.4GHz profile: nmcli connection clone \"$CURRENT_SSID\" \"${CURRENT_SSID}_24G\"" + echo " 🔧 Configure 2.4GHz-only: nmcli connection modify \"${CURRENT_SSID}_24G\" 802-11-wireless.band bg" + fi + echo "" + + # Advanced DFS recommendations + echo "🎯 ADVANCED DFS MITIGATION:" + echo "" + echo "1. Router-side DFS configuration:" + echo " 🔧 Disable band steering (prevents automatic DFS channel selection)" + echo " 🔧 Set static channels: Use channel planning tool to avoid DFS" + echo " 🔧 Regional optimization: Verify router region matches your location" + echo "" + echo "2. Professional environments:" + echo " 🔧 Site survey: Use WiFi analyzer to map DFS usage patterns" + echo " 🔧 Channel planning: Coordinate with neighboring networks" + echo " 🔧 Enterprise APs: Use DFS-aware management systems" + echo "" + + if [ "$DFS_RADAR_EVENTS" -gt 0 ]; then + echo "🚨 RADAR ENVIRONMENT SPECIFIC:" + echo "" + echo "1. Identify radar sources:" + echo " 📡 Weather radar (permanent, predictable patterns)" + echo " 📡 Military radar (temporary, high impact)" + echo " 📡 Aviation radar (permanent near airports)" + echo "" + echo "2. Mitigation strategies:" + echo " 🔧 Relocate equipment away from radar sources" + echo " 🔧 Use directional antennas to reduce radar reception" + echo " 🔧 Implement automatic channel switching (smart routers)" + echo "" + fi +} + +# Dedicated DFS Channel Monitor - COMPLETE +dfs_channel_monitor() { + echo -e "${BOLD}${MAGENTA}📡 === DFS CHANNEL MONITOR ===${NC}" + echo "🔍 Dedicated Dynamic Frequency Selection and radar interference analysis" + echo "" + + # Quick system check first + gather_system_intelligence >/dev/null 2>&1 + + # Run comprehensive DFS analysis + analyze_dfs_channels + + echo "" + provide_dfs_recommendations + + echo "" + echo -e "${CYAN}🔍 === DFS MONITORING COMMANDS ===${NC}" + echo "" + echo "Real-time DFS monitoring commands you can run:" + echo "" + echo "1. Monitor for radar events:" + echo " 📋 REALTIME: journalctl -f | grep -iE 'radar|dfs|cac'" + echo "" + echo "2. Watch channel changes:" + echo " 📋 REALTIME: watch -n 2 'iw dev $IFACE link | grep freq'" + echo "" + echo "3. Scan for DFS channels in area:" + echo " 📋 PERIODIC: iw dev $IFACE scan | grep -E 'freq: 5[2-6][0-9][0-9]|freq: 51[0-9][0-9]|SSID'" + echo "" + echo "4. Check regulatory domain:" + echo " 📋 STATUS: iw reg get" + echo "" + echo "5. Monitor connection stability:" + echo " 📋 CONTINUOUS: ping -i 1 8.8.8.8 | while read pong; do echo \"\$(date): \$pong\"; done" + echo "" + + if [ "$DFS_CURRENT_CONNECTION" = true ]; then + echo -e "${RED}🚨 IMMEDIATE ACTION ITEMS:${NC}" + echo "" + echo "Your current connection uses a DFS channel. To eliminate radar-related" + echo "disconnections, implement these fixes immediately:" + echo "" + echo "Router-side fixes (RECOMMENDED):" + echo "• Change router channel to: 36, 40, 44, 48 (low 5GHz, no DFS)" + echo "• Alternative channels: 149, 153, 157, 161, 165 (high 5GHz, no DFS)" + echo "• Disable automatic channel selection" + echo "• Disable DFS channels entirely if router supports it" + echo "" + echo "Client-side temporary fix:" + if [ -n "$CURRENT_SSID" ]; then + echo "sudo nmcli connection modify \"$CURRENT_SSID\" 802-11-wireless.band bg" + echo "sudo nmcli connection up \"$CURRENT_SSID\"" + echo "(Forces 2.4GHz connection - no DFS channels exist in 2.4GHz)" + fi + echo "" + fi + + echo -e "${GREEN}💡 DFS-Free Future: WiFi 6E/7 with 6GHz${NC}" + echo "" + echo "The 6GHz band (WiFi 6E/7) contains NO DFS channels:" + echo "• All 6GHz channels (1-233) are DFS-free" + echo "• No radar interference possible" + echo "• Clean spectrum with minimal congestion" + echo "• Requires WiFi 6E/7 hardware and router" + echo "" + + if [ "$SUPPORTS_WIFI7" = true ] || echo "$CHIP_MODEL" | grep -qi "6E"; then + echo "✅ Your hardware supports 6GHz!" + echo "💡 Upgrade to WiFi 6E/7 router for DFS-free operation" + else + echo "💡 Consider WiFi 6E/7 hardware upgrade for DFS-free future" + fi + + echo "" + echo -e "${CYAN}🔧 === SMART CHANNEL SWITCHING ===${NC}" + echo "" + echo "Would you like to run the Smart Channel Switcher to avoid DFS issues? [y/N]" + read -r switcher_choice + + if [[ "$switcher_choice" =~ ^[Yy]$ ]]; then + echo "" + smart_channel_switcher + else + echo "" + echo "💡 You can manually run channel switching commands:" + echo " • Emergency 2.4GHz: sudo nmcli connection modify \"NETWORK_NAME\" 802-11-wireless.band bg" + echo " • Safe 5GHz channels: 36, 40, 44, 48, 149, 153, 157, 161, 165" + echo " • Example: sudo nmcli connection modify \"NETWORK_NAME\" 802-11-wireless.channel 44" + fi +} + +# Smart DFS Channel Switcher - COMPLETE +smart_channel_switcher() { + echo -e "${BOLD}${GREEN}🔧 === SMART DFS CHANNEL SWITCHER ===${NC}" + echo "🎯 Intelligent channel switching to avoid DFS interference" + echo "" + + # Get current active connection + ACTIVE_CONNECTION=$(nmcli -t connection show --active | grep -E "wifi|802-11-wireless" | head -1 | cut -d: -f1) + + if [ -z "$ACTIVE_CONNECTION" ]; then + echo -e "${RED}❌ No active WiFi connection detected${NC}" + echo "💡 Connect to WiFi first, then run this tool" + return 1 + fi + + echo "📡 Current active connection: $ACTIVE_CONNECTION" + + # Get current connection details + CURRENT_CHANNEL=$(nmcli connection show "$ACTIVE_CONNECTION" | grep "802-11-wireless.channel:" | awk '{print $2}') + CURRENT_BAND=$(nmcli connection show "$ACTIVE_CONNECTION" | grep "802-11-wireless.band:" | awk '{print $2}') + CURRENT_FREQ_LINK=$(iw dev "$IFACE" link 2>/dev/null | grep "freq:" | awk '{print $2}') + + echo "📊 Current configuration:" + echo " Channel setting: ${CURRENT_CHANNEL:-auto}" + echo " Band setting: ${CURRENT_BAND:-auto}" + echo " Actual frequency: ${CURRENT_FREQ_LINK:-unknown} MHz" + + # Analyze current DFS status + local current_dfs_risk="Unknown" + if [ -n "$CURRENT_FREQ_LINK" ]; then + FREQ_INT=$(printf "%.0f" "$CURRENT_FREQ_LINK" 2>/dev/null || echo "$CURRENT_FREQ_LINK" | cut -d'.' -f1) + CHANNEL_NUM=$(freq_to_channel "$FREQ_INT") + + if is_dfs_channel "$CHANNEL_NUM"; then + current_dfs_risk="HIGH - Currently on DFS channel $CHANNEL_NUM" + else + current_dfs_risk="LOW - Currently on non-DFS channel $CHANNEL_NUM" + fi + fi + + echo " DFS Risk: $current_dfs_risk" + echo "" + + # Safe channel recommendations based on scan results + echo "🔍 Analyzing environment for optimal safe channels..." + echo "" + + # Define safe channels with priorities + declare -A SAFE_CHANNELS + SAFE_CHANNELS[36]="5180" + SAFE_CHANNELS[40]="5200" + SAFE_CHANNELS[44]="5220" + SAFE_CHANNELS[48]="5240" + SAFE_CHANNELS[149]="5745" + SAFE_CHANNELS[153]="5765" + SAFE_CHANNELS[157]="5785" + SAFE_CHANNELS[161]="5805" + SAFE_CHANNELS[165]="5825" + + # Analyze congestion on safe channels + echo "📊 Safe channel analysis:" + for channel in 36 40 44 48 149 153 157 161 165; do + freq=${SAFE_CHANNELS[$channel]} + # Count networks on this frequency + count=$(echo "$SCAN_RESULTS" | grep "freq: $freq" | wc -l 2>/dev/null || echo "0") + + if [ "$count" -eq 0 ]; then + echo -e " ${GREEN}✅ Channel $channel ($freq MHz): CLEAR (0 networks)${NC}" + elif [ "$count" -le 2 ]; then + echo -e " ${YELLOW}📊 Channel $channel ($freq MHz): Light usage ($count networks)${NC}" + else + echo -e " ${RED}🚨 Channel $channel ($freq MHz): Congested ($count networks)${NC}" + fi + done + + echo "" + echo -e "${CYAN}🎯 === CHANNEL SWITCHING OPTIONS ===${NC}" + echo "" + + # Option 1: Emergency 2.4GHz fallback + echo "1) 🚨 EMERGENCY: Force 2.4GHz (guaranteed DFS-free)" + echo " Command: sudo nmcli connection modify \"$ACTIVE_CONNECTION\" 802-11-wireless.band bg" + echo " Effect: Immediate stability, reduced speed" + echo " Use when: Frequent disconnections, need immediate fix" + echo "" + + # Option 2: Best available safe 5GHz channel + echo "2) 🎯 OPTIMAL: Switch to best available safe 5GHz channel" + + # Find least congested safe channel + best_channel="" + min_count=999 + for channel in 44 36 40 48 157 149 153 161 165; do # Prioritize 44 and 157 + freq=${SAFE_CHANNELS[$channel]} + count=$(echo "$SCAN_RESULTS" | grep "freq: $freq" | wc -l 2>/dev/null || echo "0") + if [ "$count" -lt "$min_count" ]; then + min_count=$count + best_channel=$channel + fi + done + + if [ -n "$best_channel" ]; then + echo " Recommended: Channel $best_channel (${SAFE_CHANNELS[$best_channel]} MHz) - $min_count networks detected" + echo " Commands:" + echo " sudo nmcli connection modify \"$ACTIVE_CONNECTION\" 802-11-wireless.channel $best_channel" + echo " sudo nmcli connection modify \"$ACTIVE_CONNECTION\" 802-11-wireless.band a" + echo " sudo nmcli connection up \"$ACTIVE_CONNECTION\"" + fi + echo "" + + # Option 3: Manual channel selection + echo "3) 🔧 MANUAL: Choose specific safe channel" + echo " Low 5GHz band (best for compatibility):" + echo " Channel 36: sudo nmcli connection modify \"$ACTIVE_CONNECTION\" 802-11-wireless.channel 36" + echo " Channel 44: sudo nmcli connection modify \"$ACTIVE_CONNECTION\" 802-11-wireless.channel 44" + echo " Channel 48: sudo nmcli connection modify \"$ACTIVE_CONNECTION\" 802-11-wireless.channel 48" + echo "" + echo " High 5GHz band (often less congested):" + echo " Channel 149: sudo nmcli connection modify \"$ACTIVE_CONNECTION\" 802-11-wireless.channel 149" + echo " Channel 157: sudo nmcli connection modify \"$ACTIVE_CONNECTION\" 802-11-wireless.channel 157" + echo " Channel 161: sudo nmcli connection modify \"$ACTIVE_CONNECTION\" 802-11-wireless.channel 161" + echo "" + echo " After setting channel: sudo nmcli connection up \"$ACTIVE_CONNECTION\"" + echo "" + + # Option 4: Reset to auto (remove manual settings) + echo "4) 🔄 RESET: Return to automatic channel selection" + echo " Commands:" + echo " sudo nmcli connection modify \"$ACTIVE_CONNECTION\" 802-11-wireless.channel ''" + echo " sudo nmcli connection modify \"$ACTIVE_CONNECTION\" 802-11-wireless.band ''" + echo " sudo nmcli connection up \"$ACTIVE_CONNECTION\"" + echo " Warning: May select DFS channels again in congested areas" + echo "" + + # Interactive execution option + echo -e "${YELLOW}💡 Would you like to execute one of these options now? [y/N]${NC}" + read -r execute_choice + + if [[ "$execute_choice" =~ ^[Yy]$ ]]; then + echo "" + echo "Select option to execute:" + echo "1) Emergency 2.4GHz" + echo "2) Optimal safe 5GHz (Channel $best_channel)" + echo "3) Manual channel (specify)" + echo "4) Reset to auto" + echo "5) Cancel" + echo "" + echo -n "Choice [1-5]: " + read -r exec_option + + case $exec_option in + 1) + echo "🚨 Switching to 2.4GHz (DFS-free)..." + sudo nmcli connection modify "$ACTIVE_CONNECTION" 802-11-wireless.band bg + sudo nmcli connection up "$ACTIVE_CONNECTION" + echo "✅ Switched to 2.4GHz band" + ;; + 2) + if [ -n "$best_channel" ]; then + echo "🎯 Switching to optimal channel $best_channel..." + sudo nmcli connection modify "$ACTIVE_CONNECTION" 802-11-wireless.channel "$best_channel" + sudo nmcli connection modify "$ACTIVE_CONNECTION" 802-11-wireless.band a + sudo nmcli connection up "$ACTIVE_CONNECTION" + echo "✅ Switched to channel $best_channel (5GHz)" + else + echo "❌ Could not determine best channel" + fi + ;; + 3) + echo -n "Enter channel number (36,40,44,48,149,153,157,161,165): " + read -r manual_channel + if [[ "$manual_channel" =~ ^(36|40|44|48|149|153|157|161|165)$ ]]; then + echo "🔧 Switching to manual channel $manual_channel..." + sudo nmcli connection modify "$ACTIVE_CONNECTION" 802-11-wireless.channel "$manual_channel" + sudo nmcli connection modify "$ACTIVE_CONNECTION" 802-11-wireless.band a + sudo nmcli connection up "$ACTIVE_CONNECTION" + echo "✅ Switched to channel $manual_channel" + else + echo "❌ Invalid channel. Must be: 36,40,44,48,149,153,157,161,165" + fi + ;; + 4) + echo "🔄 Resetting to automatic selection..." + sudo nmcli connection modify "$ACTIVE_CONNECTION" 802-11-wireless.channel "" + sudo nmcli connection modify "$ACTIVE_CONNECTION" 802-11-wireless.band "" + sudo nmcli connection up "$ACTIVE_CONNECTION" + echo "✅ Reset to automatic channel selection" + ;; + 5) + echo "Cancelled - no changes made" + ;; + *) + echo "❌ Invalid option" + ;; + esac + + if [[ "$exec_option" =~ ^[1-4]$ ]]; then + echo "" + echo "🔍 Waiting 10 seconds for connection to stabilize..." + sleep 10 + echo "📊 New connection status:" + iw dev "$IFACE" link 2>/dev/null | grep -E "Connected|freq|signal" || echo "Connection info not available" + fi + fi + + echo "" + echo -e "${GREEN}💡 TIP: Save these commands for future use!${NC}" + echo "📋 You can run these commands anytime DFS issues occur" +} + +# Main execution with modern error checking +main() { + # Check for required modern tools + if ! command -v iw >/dev/null 2>&1; then + echo -e "${RED}Error: 'iw' command not found${NC}" + echo "Install with your package manager:" + + detect_distribution + case "$DISTRO_ID" in + "fedora"|"rhel"|"centos"|"rocky"|"almalinux") + echo "sudo dnf install iw wireless-tools" + ;; + "ubuntu"|"debian"|"pop"|"mint") + echo "sudo apt install iw wireless-tools" + ;; + "arch"|"manjaro"|"endeavouros") + echo "sudo pacman -S iw wireless_tools" + ;; + *) + echo "Use your distribution's package manager to install 'iw'" + ;; + esac + exit 1 + fi + + # Check for modern kernel + KERNEL_VERSION=$(uname -r) + KERNEL_MAJOR=$(echo "$KERNEL_VERSION" | cut -d'.' -f1) + KERNEL_MINOR=$(echo "$KERNEL_VERSION" | cut -d'.' -f2) + + if [ "$KERNEL_MAJOR" -lt 6 ]; then + echo -e "${YELLOW}⚠️ Warning: Kernel $KERNEL_VERSION detected${NC}" + echo "For best WiFi 7/6E support, consider kernel 6.8+ or newer" + echo "" + fi + + while true; do + show_menu + read -r choice + + case $choice in + 1) + complete_wifi_analysis | tee "wifi_analysis_2025_$(date +%Y%m%d_%H%M%S).log" + echo "" + echo "Press Enter to continue..." + read -r + ;; + 2) + error_analysis_troubleshooting | tee "wifi_errors_2025_$(date +%Y%m%d_%H%M%S).log" + echo "" + echo "Press Enter to continue..." + read -r + ;; + 3) + interactive_workaround_generator + echo "" + echo "Press Enter to continue..." + read -r + ;; + 4) + dfs_channel_monitor | tee "dfs_analysis_$(date +%Y%m%d_%H%M%S).log" + echo "" + echo "Press Enter to continue..." + read -r + ;; + 5) + tx_power_band_test | tee "tx_power_test_$(date +%Y%m%d_%H%M%S).log" + echo "" + echo "Press Enter to continue..." + read -r + ;; + 6) + manual_band_switching | tee "band_switching_$(date +%Y%m%d_%H%M%S).log" + echo "" + echo "Press Enter to continue..." + read -r + ;; + 7) + echo -e "${GREEN}🎯 WiFi Analysis complete! Modern networking with DFS monitoring awaits.${NC}" + echo "Thank you for using the Enhanced WiFi Analyzer with DFS support!" + exit 0 + ;; + *) + echo -e "${RED}Invalid option${NC}" + sleep 1 + ;; + esac + done +} + +main "$@" diff --git a/FPR-320-firmware/readme.md b/FPR-320-firmware/readme.md new file mode 100644 index 0000000..1f905de --- /dev/null +++ b/FPR-320-firmware/readme.md @@ -0,0 +1,123 @@ + +## Framework Laptop Fingerprint Readers with 01000320 Firmware Update Guide +### A step by step guide +#### This assumes you are on a disro such as Ubuntu LTS or Fedora, and have libfprint version of **at least** v1.92.0 or newer. + + +- Verify that this is in fact, firmare version 01000320. +- You must use a distro that is has a libfprint version of **at least** v1.92.0 or **newer**. Fully package updated Ubuntu LTS and Fedora Workstation will meet this requirement. Other distros, even based on these distros, may or may not. +- You will need to open a terminal from your launcher, then paste in the provided lines of code to get validation of firmware version, update it and so forth. +- The tiny icons on the right side of each code box allow you to easily click it, to copy the code. Then you can right click paste the code into the terminal. If it errors out, try Ctrl Shift V instead to paste. + +- Let's check your firmware version, verify it's 01000320. + +``` +fwupdmgr get-devices | awk '/Fingerprint Sensor:/{flag=1} flag; /Device Flags:/{flag=0}' +``` + +- If it comes back with "Current version: 01000320", then continue with this guide. + +- Making sure we are using the correct **GUID** from the output above: + +- To verify you have the correct GUID, you can run this to verify it's correct with the output from this code: + +``` +fwupdmgr get-devices | awk '/Fingerprint Sensor:/{flag=1} flag; /Device Flags:/{flag=0}' | grep 'GUID:' | awk -F'GUID: ' '{print $2}' | awk '{print $1}' +``` + +The GUID should return with: 1e8c8470-a49c-571a-82fd-19c9fa32b8c3. With the GUID verified: + +``` +fwupdmgr get-devices 1e8c8470-a49c-571a-82fd-19c9fa32b8c3 +``` + +- Let's enable testing as at this time, lvfs testing has the firmware. + +``` +fwupdmgr enable-remote lvfs-testing +``` + +- Now refresh everything: + +``` +fwupdmgr refresh --force +``` + +- Let's try to update the firmeware now. + +``` +fwupdmgr update 1e8c8470-a49c-571a-82fd-19c9fa32b8c3 +``` + +You will likely see something like this: + +``` +╔════════════════════════════════════════════════════════════════════════════╗ +║ Upgrade Fingerprint Sensor from 01000320 to 01000334? ║ +╠════════════════════════════════════════════════════════════════════════════╣ +║ Fix physical MITM vulnerability that was found from blackwinghq - a touch ║ +║ of pwn part 1. ║ +║ ║ +║ Fingerprint Sensor and all connected devices may not be usable while ║ +║ updating. ║ +╚════════════════════════════════════════════════════════════════════════════╝ +Perform operation? [Y|n]: +Writing… [************************************ ] +failed to write: failed to reply: transfer timed out + +> fwupdmgr get-devices 1e8c8470-a49c-571a-82fd-19c9fa32b8c3 +Selected device: Fingerprint Sensor +Framework Laptop (12th Gen Intel Core) +│ +└─Fingerprint Sensor: + Device ID: d432baa2162a32c1554ef24bd8281953b9d07c11 + Summary: Match-On-Chip fingerprint sensor + Current version: 01000320 + Vendor: Goodix (USB:0x27C6) + Install Duration: 10 seconds + Serial Number: UIDXXXXXXXX_XXXX_MOC_B0 + Update State: Failed + Problems: • An update is in progress + Last modified: 2024-08-30 08:20 + GUID: 1e8c8470-a49c-571a-82fd-19c9fa32b8c3 ← USB\VID_27C6&PID_609C + Device Flags: • Supported on remote server + • Device stages updates + • Device can recover flash failures + • Updatable + • Signed Payload +``` + +- Note the update **status of failed**. We can verify again this with: + +``` +fwupdmgr get-devices 1e8c8470-a49c-571a-82fd-19c9fa32b8c3 +``` + +This will likely **still reflect the old 01000320 firmware**. + +- At this stage, **reboot** your laptop. + +- Now run this again: + +``` +fwupdmgr get-devices 1e8c8470-a49c-571a-82fd-19c9fa32b8c3 +``` + +- At this point, you should be looking at Current version: 01000334 + +- From here, we can enroll fingerprints from GNOME on Ubuntu LTS or Fedora Workstation. + +#### Troubleshooting + +- You may find it times out. Let it sit for a few minutes, then sudo sytemctl reboot -i +- fwupdmgr get-devices 1e8c8470-a49c-571a-82fd-19c9fa32b8c3 again. +- fwupdmgr update 1e8c8470-a49c-571a-82fd-19c9fa32b8c3 again. +- This may take up to three times, but it will eventually go. + +#### Return to the Fingerprint Troubleshooting guides + +- Return to the [Ubuntu Fingerprint Troubleshooting](https://knowledgebase.frame.work/en_us/ubuntu-fingerprint-troubleshooting-r1_DA0TMn) or [Fedora Fingerprint Troubleshooting](https://knowledgebase.frame.work/en_us/fedora-fingerprint-troubleshooting-SyfIAyCM3) guides. + +#### References + +This update is listed on the [LVFS website](https://fwupd.org/lvfs/devices/work.frame.goodixmoc19c9fa32b8c3.firmware) diff --git a/Fedora40-fw16.md b/Fedora40-fw16.md index 5c65a8d..92ec319 100644 --- a/Fedora40-fw16.md +++ b/Fedora40-fw16.md @@ -77,7 +77,7 @@ sudo dnf install nvtop Create a script with the following: ``` -sudo /usr/local/bin/external_video.sh +sudo nano /usr/local/bin/external_video.sh ``` Paste in: @@ -89,7 +89,14 @@ timeout 2 nvtop echo "nvtop run completed." ``` -Save the file. Now setup a udev rule. + +Save the file. Then set it to executable. + +``` +sudo chmod +x /usr/local/bin/external_video.sh +``` + +Now setup a udev rule. ``` sudo nano /etc/udev/rules.d/99-external_video.rules ``` @@ -159,3 +166,15 @@ sudo dnf install gnome-tweaks -y       + +---------------------------------------- + +## Framework Laptop 16 not providing all of the expected refresh rates in kernels 6.9 and up. + +[Framework Laptop 16 not providing all of the expected refresh rates ](https://github.com/FrameworkComputer/linux-docs/blob/main/amdgpu-workarounds/amdgpu_freesync_video/amdgpu_freesync_video.md#amdgpufreesync_video1-parameter-workaround-framework-laptop-16-only) + +  +  +   +  +  diff --git a/Fingerprint-Checker/fpr-checker.sh b/Fingerprint-Checker/fpr-checker.sh new file mode 100755 index 0000000..3855400 --- /dev/null +++ b/Fingerprint-Checker/fpr-checker.sh @@ -0,0 +1,144 @@ +#!/usr/bin/env bash + +# fpr-checker.sh - A script to manage fingerprint data using fprintd with a selectable menu. + +# Colors for output +RED=$(tput setaf 1) +GREEN=$(tput setaf 2) +YELLOW=$(tput setaf 3) +RESET=$(tput sgr0) + +# Determine the actual user invoking the script, whether through sudo or not +if [ -n "$SUDO_USER" ]; then + USER=$SUDO_USER +else + USER=$(whoami) +fi + +# Function to detect the desktop environment +detect_desktop_environment() { + if [ "$XDG_CURRENT_DESKTOP" ]; then + echo "${YELLOW}Desktop Environment: $XDG_CURRENT_DESKTOP${RESET}" + if [[ "$XDG_CURRENT_DESKTOP" != *"GNOME"* ]]; then + echo "${RED}Note: Fingerprint login might not work with this desktop environment, but you can still configure sudo to work with fingerprints.${RESET}" + fi + else + echo "${YELLOW}Desktop Environment: Unknown${RESET}" + fi +} + +# Function to enroll a specific finger for the actual user +enroll_finger() { + local finger=$1 + echo "${YELLOW}Enrolling $finger for user $USER...${RESET}" + sudo -u "$USER" fprintd-enroll -f "$finger" + if [ $? -eq 0 ]; then + echo "${GREEN}Fingerprint enrolled successfully for $finger.${RESET}" + else + echo "${RED}Failed to enroll fingerprint for $finger.${RESET}" + fi + read -p "Press [Enter] key to continue..." # Pause to let the user see the output +} + +# Function to get standard Linux users (UID between 1000 and 60000) +get_standard_users() { + awk -F: '($3 == 0 || ($3 >= 1000 && $3 <= 60000)) {print $1}' /etc/passwd +} + +# Function to delete all fingerprints for standard Linux users +delete_all_fingerprints() { + echo "${RED}Deleting all fingerprints for standard Linux users...${RESET}" + local deleted_any=0 + + for user in $(get_standard_users); do + sudo fprintd-delete "$user" 2>/dev/null + if [ $? -eq 0 ]; then + echo "${GREEN}All fingerprints deleted successfully for user: $user${RESET}" + deleted_any=1 + else + echo "${YELLOW}No fingerprints found for user: $user${RESET}" + fi + done + + if [ $deleted_any -eq 0 ]; then + echo "${YELLOW}No fingerprints were found to delete for standard Linux users.${RESET}" + fi + + read -p "Press [Enter] key to continue..." # Pause to let the user see the output +} + +# Function to list registered fingerprints for standard Linux users +list_fingerprints_for_all() { + echo "${YELLOW}Listing registered fingerprints for standard Linux users...${RESET}" + local registered=0 + + for user in $(get_standard_users); do + output=$(sudo fprintd-list "$user" 2>/dev/null) + + if [[ "$output" != *"no fingers enrolled"* && -n "$output" ]]; then + echo "${GREEN}Fingerprints for user: $user${RESET}" + echo "$output" + echo + registered=1 + fi + done + + if [ $registered -eq 0 ]; then + echo "${YELLOW}No fingerprints registered for any standard Linux users.${RESET}" + fi + + read -p "Press [Enter] key to continue..." # Pause to let the user see the output +} + +# Main menu function +show_menu() { + clear + detect_desktop_environment + echo "====================================" + echo " Fingerprint Management Script" + echo "====================================" + echo "1. Enroll Left Thumb" + echo "2. Enroll Left Index Finger" + echo "3. Enroll Left Middle Finger" + echo "4. Enroll Left Ring Finger" + echo "5. Enroll Left Little Finger" + echo "6. Enroll Right Thumb" + echo "7. Enroll Right Index Finger" + echo "8. Enroll Right Middle Finger" + echo "9. Enroll Right Ring Finger" + echo "10. Enroll Right Little Finger" + echo "11. Delete All Fingerprints for Standard Users" + echo "12. List Registered Fingerprints for Standard Users" + echo "13. Exit" + echo "====================================" + echo -n "Choose an option: " +} + +# Function to handle user input +read_options() { + local choice + read -r choice + case $choice in + 1) enroll_finger "left-thumb" ;; + 2) enroll_finger "left-index-finger" ;; + 3) enroll_finger "left-middle-finger" ;; + 4) enroll_finger "left-ring-finger" ;; + 5) enroll_finger "left-little-finger" ;; + 6) enroll_finger "right-thumb" ;; + 7) enroll_finger "right-index-finger" ;; + 8) enroll_finger "right-middle-finger" ;; + 9) enroll_finger "right-ring-finger" ;; + 10) enroll_finger "right-little-finger" ;; + 11) delete_all_fingerprints ;; + 12) list_fingerprints_for_all ;; + 13) exit 0 ;; + *) echo "${RED}Invalid option!${RESET}" && sleep 2 + esac +} + +# Main script loop +while true +do + show_menu + read_options +done diff --git a/Fingerprint-Checker/images/checker.png b/Fingerprint-Checker/images/checker.png new file mode 100644 index 0000000..977e016 Binary files /dev/null and b/Fingerprint-Checker/images/checker.png differ diff --git a/Fingerprint-Checker/images/readme b/Fingerprint-Checker/images/readme new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/Fingerprint-Checker/images/readme @@ -0,0 +1 @@ + diff --git a/Fingerprint-Checker/readme.md b/Fingerprint-Checker/readme.md new file mode 100644 index 0000000..d769f29 --- /dev/null +++ b/Fingerprint-Checker/readme.md @@ -0,0 +1,91 @@ +## Fingerprint Checker + +Fingerprint Checker is merely a friendly terminal front end to [fprintd](https://fprint.freedesktop.org/) + +### Desktop Environment Detection + +- **Highlight:** The script automatically detects the current desktop environment and displays it at the top of the menu in yellow. +- **Desktop Environment Detection:** If the desktop environment is not GNOME, the script warns the user that fingerprint login might not work, but configuring sudo with fingerprint authentication is still possible. + +### User-Friendly Menu: + +- **Clear Interface:** The script presents a clear and simple menu that allows users to manage their fingerprint data through options like listing, enrolling, deleting, and verifying fingerprints. +- **Highlighted Output:** Important outputs, such as the desktop environment, fingerprint entries, and verification processes, are highlighted in yellow for easy visibility. + +### Fingerprint Management + +- **Listing Fingerprints:** Users can list all enrolled fingerprints for the current or a specified user. +- **Enrolling Fingerprints:** The script supports enrolling new fingerprints for the current user. +- **Deleting Fingerprints:** Users can delete all fingerprints for users. +- **Verifying Fingerprints:** The script allows users to verify an enrolled fingerprint for the current user, with the verification process highlighted in yellow. + + +### Error Handling and Feedback: + +- **Input Validation:** The script handles invalid input gracefully, providing feedback and re-prompting the user when necessary. +Pausing for Review: After each operation, the script pauses and prompts the user to press Enter, ensuring they have time to review the output before returning to the menu. + +![Fingerprint Checker](https://raw.githubusercontent.com/FrameworkComputer/linux-docs/main/Fingerprint-Checker/images/checker.png) + +------------------------------------------------------------- + +### Install Curl + +Curl should already be installed, but just in case: + +#### Fedora +``` +sudo dnf install curl -y +``` + +or + +#### Ubuntu +``` +sudo apt install curl -y +``` + +### To Install, simply run: + +``` +curl -s https://raw.githubusercontent.com/FrameworkComputer/linux-docs/main/Fingerprint-Checker/fpr-checker.sh -o fpr-checker.sh && clear && bash fpr-checker.sh +``` + +
+ +#### Running the script in the future + +>After the install, you can run going forward with the following in the HOME directory. So merely opening a terminal and running this will work if the original script has not been moved.
+ +``` +bash fpr-checker.sh +``` + +

+ +------------------------------------------------------------- + +### FAQ + +- _Why do we need this?_ + +
+You likely do not, but, if you find that you get your finterprint reader to detect your prints, this is a little more user friendly than using fprintd-list, fprintd-enroll, fprintd-delete and fprintd-verify. +

+ +- _I would rather do this the manual way._ + +
+Great, simply use fprintd-list, fprintd-enroll, fprintd-delete and fprintd-verify and $USER for each command. +
+ +Example: + +``` +fprintd-verify $USER +``` + + +#### Return to the Fingerprint Troubleshooting guides + +- Return to the [Ubuntu Fingerprint Troubleshooting](https://knowledgebase.frame.work/en_us/ubuntu-fingerprint-troubleshooting-r1_DA0TMn) or [Fedora Fingerprint Troubleshooting](https://knowledgebase.frame.work/en_us/fedora-fingerprint-troubleshooting-SyfIAyCM3) guides. diff --git a/Fingerprint-Wake-Workaround/README.md b/Fingerprint-Wake-Workaround/README.md new file mode 100644 index 0000000..b9e36b9 --- /dev/null +++ b/Fingerprint-Wake-Workaround/README.md @@ -0,0 +1,88 @@ +# Framework 13 Ryzen AI 300 series Fingerprint Wake Workaround + +Automatic workaround for Framework 13 AMD fingerprint reader not working after suspend/resume. + +**IMPORTANT**: In our testing, this has not been needed. Had it occur once in 30 suspend to resumes - most folks should not need this. + +## Problem + +On Framework 13 AMD laptops, the Goodix fingerprint reader sometimes fails to reconnect after waking from suspend. This requires a manual reboot to restore functionality. + +**Tracking Issue**: https://github.com/FrameworkComputer/SoftwareFirmwareIssueTracker/issues/102 + +## Solution + +This script automatically detects your fingerprint reader hardware and creates a systemd service that: +- Monitors for the fingerprint reader after each wake +- Automatically resets the USB controller if the reader is missing +- Restarts the fprintd service to restore functionality +- Logs all actions for debugging + +**No hardcoded values** - works across different Framework 13 AMD configurations and fingerprint reader variants (Goodix, Synaptics, ELAN). + +## Supported Hardware + +- **Laptop**: Framework 13 AMD (Ryzen AI 300 series) +- **Fingerprint Reader**: Goodix Fingerprint Reader +- **Distributions**: Any systemd-based Linux distribution + +## Installation + +One-line install: + + curl -sSL https://raw.githubusercontent.com/FrameworkComputer/linux-docs/refs/heads/main/Fingerprint-Wake-Workaround/install-fingerprint-wake.sh | bash + +The installer will auto-detect your hardware and show you exactly what was found. + +## Testing + +After installation, test the workaround: + + journalctl -t fp-rebind -f + +Then suspend your system and resume. Check if fingerprint reader works and view the logs. + +## Uninstallation + +One-line uninstall: + + curl -sSL https://raw.githubusercontent.com/FrameworkComputer/linux-docs/refs/heads/main/Fingerprint-Wake-Workaround/uninstall-fingerprint-wake.sh | bash + +## How It Works + +1. **Detection Phase**: Scans USB devices to find your fingerprint reader +2. **Path Resolution**: Follows sysfs symlinks to find the PCI controller +3. **Driver Detection**: Identifies which driver (xhci_hcd/xhci-pci) is in use +4. **Service Creation**: Generates a systemd service with detected values +5. **Wake Monitoring**: After each suspend/resume, checks if reader is present +6. **Automatic Recovery**: If missing, unbinds and rebinds the USB controller + +## Files Created + +- `/etc/local/bin/run-after-wake-for-fprint.sh` - Wake monitoring script +- `/etc/systemd/system/run-after-wake-for-fprint.service` - Systemd service unit + +## Troubleshooting + +View service logs: + + journalctl -t fp-rebind --since "24 hours ago" + +Check service status: + + systemctl status run-after-wake-for-fprint.service + +Test the script manually: + + sudo systemctl start run-after-wake-for-fprint.service + + +## Credits + +Inspired by kariudo's original implementation: https://github.com/kariudo/framework-13-fingerprint-fix + +This version improves upon the original by: +- Auto-detecting all hardware values instead of hardcoding +- Supporting multiple fingerprint reader variants +- Working across different PCI configurations +- Providing detailed diagnostic output during installation diff --git a/Fingerprint-Wake-Workaround/install-fingerprint-wake.sh b/Fingerprint-Wake-Workaround/install-fingerprint-wake.sh new file mode 100644 index 0000000..f133147 --- /dev/null +++ b/Fingerprint-Wake-Workaround/install-fingerprint-wake.sh @@ -0,0 +1,244 @@ +#!/bin/bash +# Framework 13 Fingerprint Wake Fix Installer +# Auto-detects hardware and installs systemd service + +set -e + +echo "Framework 13 Fingerprint Wake Fix Installer" +echo "============================================" +echo "" + +# Remove any existing installation first +if [ -f /etc/systemd/system/run-after-wake-for-fprint.service ]; then + echo "⚠ Found existing installation - removing..." + sudo systemctl disable --now run-after-wake-for-fprint.service 2>/dev/null || true + sudo rm -f /etc/systemd/system/run-after-wake-for-fprint.service + sudo rm -f /etc/local/bin/run-after-wake-for-fprint.sh + sudo systemctl daemon-reload + echo "✓ Removed old installation" + echo "" +fi + +# 1. Detect fingerprint reader +echo "[1/6] Detecting fingerprint reader..." +echo "" + +FPRINT_DEVICE=$(lsusb | grep -E "Goodix.*Fingerprint|27c6:609c" | head -1) + +if [ -z "$FPRINT_DEVICE" ]; then + echo "✗ NO fingerprint reader found" + echo "" + echo "All USB devices detected:" + lsusb + echo "" + echo "Troubleshooting:" + echo " • Enable fingerprint reader in BIOS/UEFI" + echo " • Check 'dmesg | grep -i fingerprint'" + exit 1 +fi + +echo "✓ Found fingerprint reader:" +echo " $FPRINT_DEVICE" + +# 2. Extract device ID +FPRINT_ID=$(echo "$FPRINT_DEVICE" | grep -oP 'ID \K[0-9a-f]{4}:[0-9a-f]{4}') +if [ -z "$FPRINT_ID" ]; then + echo "✗ Could not parse device ID" + exit 1 +fi + +echo "✓ Device ID: $FPRINT_ID" + +# 3. Get bus number +BUS_RAW=$(echo "$FPRINT_DEVICE" | awk '{print $2}') +BUS=$((10#$BUS_RAW)) + +echo "✓ USB Bus: $BUS (raw: $BUS_RAW)" + +# 4. Find USB controller in sysfs +echo "" +echo "[2/6] Resolving USB controller path..." +echo "" + +USB_DEVICE_PATH="/sys/bus/usb/devices/usb${BUS}" + +if [ ! -d "$USB_DEVICE_PATH" ]; then + echo "✗ USB path not found: $USB_DEVICE_PATH" + exit 1 +fi + +echo "✓ USB device path exists: $USB_DEVICE_PATH" + +# 5. Follow symlink to PCI controller +USB_PATH=$(readlink -f "$USB_DEVICE_PATH") +echo "✓ Resolved path: $USB_PATH" + +PCI_FUNC=$(echo "$USB_PATH" | grep -oP '[0-9a-f]{4}:[0-9a-f]{2}:[0-9a-f]{2}\.[0-9]' | tail -1) + +if [ -z "$PCI_FUNC" ]; then + echo "✗ Could not determine PCI controller from path" + exit 1 +fi + +echo "✓ PCI Controller: $PCI_FUNC" + +# 6. Verify PCI device exists +PCI_DEVICE_PATH="/sys/bus/pci/devices/$PCI_FUNC" + +if [ ! -d "$PCI_DEVICE_PATH" ]; then + echo "✗ PCI device path not found: $PCI_DEVICE_PATH" + exit 1 +fi + +echo "✓ PCI device path verified" + +# 7. Detect driver +echo "" +echo "[3/6] Detecting driver..." +echo "" + +DRIVER_LINK="$PCI_DEVICE_PATH/driver" + +if [ ! -L "$DRIVER_LINK" ]; then + echo "✗ No driver bound to $PCI_FUNC" + exit 1 +fi + +DRIVER_NAME=$(basename "$(readlink -f "$DRIVER_LINK")") +DRIVER_PATH="/sys/bus/pci/drivers/$DRIVER_NAME" + +echo "✓ Driver detected: $DRIVER_NAME" +echo "✓ Driver path: $DRIVER_PATH" + +# 8. Verify it's xHCI +if [[ ! "$DRIVER_NAME" =~ xhci ]]; then + echo "" + echo "⚠ WARNING: '$DRIVER_NAME' doesn't look like an xHCI driver" + echo " Expected: xhci_hcd or xhci-pci" + echo " This script may not work correctly" + echo "" + read -p "Continue anyway? (y/N) " -n 1 -r + echo + if [[ ! $REPLY =~ ^[Yy]$ ]]; then + echo "Installation cancelled" + exit 1 + fi +fi + +# 9. Check fprintd status +echo "" +echo "[4/6] Checking fprintd status..." +echo "" + +if systemctl is-active --quiet fprintd.service; then + echo "⚠ fprintd service is running (this is not expected and should be reported to support, something might be stuck)" +else + echo "✓ fprintd service not running (this is correct and expected)" +fi + +if command -v fprintd-list &>/dev/null; then + if fprintd-list 2>/dev/null | grep -q "fingerprint"; then + echo "✓ fprintd recognizes fingerprint device" + fi +fi + +# 10. Create wake script +echo "" +echo "[5/6] Creating wake script..." +echo "" + +sudo mkdir -p /etc/local/bin + +sudo tee /etc/local/bin/run-after-wake-for-fprint.sh > /dev/null << EOF +#!/bin/sh +# Framework fingerprint wake fix +# Generated for: $FPRINT_ID on $PCI_FUNC + +PCI_FUNC="$PCI_FUNC" +FPRINT_ID="$FPRINT_ID" +DRIVER_PATH="$DRIVER_PATH" + +logger -t fp-rebind "Checking fingerprint reader after wake" + +sleep 2 + +if ! lsusb -d "\$FPRINT_ID" >/dev/null 2>&1; then + logger -t fp-rebind "Fingerprint missing, resetting controller \$PCI_FUNC" + + # Unbind + if ! echo "\$PCI_FUNC" >"\$DRIVER_PATH/unbind" 2>/dev/null; then + logger -t fp-rebind "ERROR: Unbind failed" + exit 1 + fi + + sleep 1 + + # Rebind + if ! echo "\$PCI_FUNC" >"\$DRIVER_PATH/bind" 2>/dev/null; then + logger -t fp-rebind "ERROR: Rebind failed" + exit 1 + fi + + sleep 2 + systemctl try-restart fprintd.service + + if lsusb -d "\$FPRINT_ID" >/dev/null 2>&1; then + logger -t fp-rebind "SUCCESS: Reader restored" + else + logger -t fp-rebind "WARNING: Reader still missing" + fi +else + logger -t fp-rebind "Reader present, no action needed" +fi +EOF + +sudo chmod +x /etc/local/bin/run-after-wake-for-fprint.sh + +echo "✓ Created: /etc/local/bin/run-after-wake-for-fprint.sh" + +# 11. Create systemd service +echo "" +echo "[6/6] Creating and enabling systemd service..." +echo "" + +sudo tee /etc/systemd/system/run-after-wake-for-fprint.service > /dev/null << 'EOF' +[Unit] +Description=Restore fingerprint reader after system resume +After=suspend.target hibernate.target hybrid-sleep.target suspend-then-hibernate.target + +[Service] +Type=oneshot +ExecStart=/etc/local/bin/run-after-wake-for-fprint.sh +StandardOutput=journal +StandardError=journal + +[Install] +WantedBy=suspend.target hibernate.target hybrid-sleep.target suspend-then-hibernate.target +EOF + +echo "✓ Created: /etc/systemd/system/run-after-wake-for-fprint.service" + +sudo systemctl daemon-reload +sudo systemctl enable run-after-wake-for-fprint.service + +echo "✓ Service enabled" + +# 12. Final summary +echo "" +echo "============================================" +echo "✓ Installation Complete!" +echo "============================================" +echo "" +echo "Detection Summary:" +echo " Device ID: $FPRINT_ID" +echo " USB Bus: $BUS" +echo " PCI Controller: $PCI_FUNC" +echo " Driver: $DRIVER_NAME" +echo "" +echo "Testing:" +echo " 1. Test now: sudo systemctl start run-after-wake-for-fprint.service" +echo " 2. Check logs: journalctl -t fp-rebind -f" +echo " 3. Suspend/wake: systemctl suspend" +echo "" +echo "The service will automatically run after each suspend/resume" +echo "" diff --git a/Fingerprint-Wake-Workaround/uninstall-fingerprint-wake.sh b/Fingerprint-Wake-Workaround/uninstall-fingerprint-wake.sh new file mode 100644 index 0000000..d0c5b3a --- /dev/null +++ b/Fingerprint-Wake-Workaround/uninstall-fingerprint-wake.sh @@ -0,0 +1,27 @@ +#!/bin/bash +# uninstall-fingerprint-fix.sh - Remove old fingerprint wake fix + +echo "Removing existing fingerprint wake fix..." + +# Stop and disable service +if systemctl is-enabled run-after-wake-for-fprint.service &>/dev/null; then + echo " Disabling service..." + sudo systemctl disable run-after-wake-for-fprint.service +fi + +if systemctl is-active run-after-wake-for-fprint.service &>/dev/null; then + echo " Stopping service..." + sudo systemctl stop run-after-wake-for-fprint.service +fi + +# Remove files +echo " Removing files..." +sudo rm -f /etc/systemd/system/run-after-wake-for-fprint.service +sudo rm -f /etc/local/bin/run-after-wake-for-fprint.sh + +# Reload systemd +sudo systemctl daemon-reload + +echo "✓ Old installation removed" +echo "" +echo "Now run the new installer to install the auto-detecting version" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/MeshAnalyzer/files/README b/MeshAnalyzer/files/README new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/MeshAnalyzer/files/README @@ -0,0 +1 @@ + diff --git a/MeshAnalyzer/files/mesh_analyzer.py b/MeshAnalyzer/files/mesh_analyzer.py new file mode 100644 index 0000000..53b1e27 --- /dev/null +++ b/MeshAnalyzer/files/mesh_analyzer.py @@ -0,0 +1,2675 @@ +#!/usr/bin/env python3 +""" +WiFi Mesh Network Analyzer - Analysis and Recommendations +- Comprehensive radio environment analysis +- Historical performance tracking +- Mesh topology intelligence with Venn overlap analysis +- Problem detection and recommendations +- HTML report generation +- Focus on accurate analysis without configuration complexity +""" + +import subprocess +import re +import time +import pickle +import os +import json +import zipfile +from datetime import datetime +from collections import defaultdict, deque +from dataclasses import dataclass, asdict +from typing import Dict, List, Set, Optional +import threading +from pathlib import Path +import logging + +# Import the new Venn calculator +try: + from mesh_venn_calculator import MeshVennCalculator +except ImportError: + # Fallback if file not found + class MeshVennCalculator: + def generate_venn_data(self, nodes_data): + return {'nodes': nodes_data, 'overlaps': [], 'total_coverage': 0} + def get_overlap_quality_assessment(self, venn_data): + return {'quality': 'unknown', 'score': 0, 'description': 'Venn calculator not available'} + +# Import the updated HTML reporter with roaming and power support +try: + from mesh_html_reporter import MeshHTMLReporter + UPDATED_HTML_REPORTER_AVAILABLE = True +except ImportError: + UPDATED_HTML_REPORTER_AVAILABLE = False + print("⚠️ Warning: mesh_html_reporter.py not found - using built-in reporter") + +# Import the roaming detector - Matt is testing some new functionality - adding two new modules for import. +try: + from mesh_roaming_detector import MeshRoamingDetector + ROAMING_DETECTOR_AVAILABLE = True +except ImportError: + ROAMING_DETECTOR_AVAILABLE = False + MeshRoamingDetector = None + +# Import the power detective +try: + from mesh_power_detective import MeshPowerDetective + POWER_DETECTIVE_AVAILABLE = True +except ImportError: + POWER_DETECTIVE_AVAILABLE = False + MeshPowerDetective = None + +@dataclass +class APScan: + ssid: str + bssid: str + freq: int + signal: int + capabilities: Set[str] + last_seen: float + + def to_dict(self): + """Convert to JSON-serializable dict""" + return { + 'ssid': self.ssid, + 'bssid': self.bssid, + 'freq': self.freq, + 'signal': self.signal, + 'capabilities': list(self.capabilities), # Convert set to list + 'last_seen': self.last_seen + } + +@dataclass +class ConnectionEvent: + timestamp: float + bssid: str + event_type: str # 'connect', 'disconnect', 'auth_timeout' + signal: int + duration: Optional[float] = None + reason: Optional[str] = None + +@dataclass +class BSSIDHistory: + bssid: str + total_connections: int = 0 + successful_connections: int = 0 + total_duration: float = 0.0 + avg_signal: float = 0.0 + signal_samples: List[tuple] = None + auth_failures: int = 0 + disconnects: int = 0 + last_seen: float = 0.0 + stability_score: float = 0.0 + + def __post_init__(self): + if self.signal_samples is None: + self.signal_samples = [] + +def make_json_serializable(obj): + """Convert object to JSON-serializable format""" + if isinstance(obj, set): + return list(obj) + elif isinstance(obj, dict): + return {k: make_json_serializable(v) for k, v in obj.items()} + elif isinstance(obj, list): + return [make_json_serializable(item) for item in obj] + elif hasattr(obj, 'to_dict'): + return obj.to_dict() + elif hasattr(obj, '__dict__'): + return make_json_serializable(obj.__dict__) + else: + return obj + +class LogManager: + """Comprehensive logging system with automatic compression""" + + def __init__(self, data_dir: Path): + self.data_dir = data_dir + self.logs_dir = data_dir / "logs" + self.logs_dir.mkdir(parents=True, exist_ok=True) + + # Create timestamp for this session + self.session_timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") + self.session_date = datetime.now().strftime("%Y-%m-%d") + + # Initialize log files + self.analysis_log = self.logs_dir / f"analysis_{self.session_timestamp}.log" + self.connections_log = self.logs_dir / f"connections_{self.session_date}.log" + self.performance_log = self.logs_dir / f"performance_{self.session_date}.log" + self.debug_log = self.logs_dir / f"debug_{self.session_date}.log" + + # Setup loggers + self.setup_loggers() + + print(f"📝 Logging enabled: {self.logs_dir}") + + def setup_loggers(self): + """Setup structured loggers for different log types""" + + # Analysis logger (detailed scan results) + self.analysis_logger = logging.getLogger('analysis') + self.analysis_logger.setLevel(logging.INFO) + analysis_handler = logging.FileHandler(self.analysis_log) + analysis_handler.setFormatter(logging.Formatter( + '%(asctime)s | %(levelname)s | %(message)s' + )) + self.analysis_logger.addHandler(analysis_handler) + + # Connection logger (WiFi events) + self.connection_logger = logging.getLogger('connections') + self.connection_logger.setLevel(logging.INFO) + connection_handler = logging.FileHandler(self.connections_log) + connection_handler.setFormatter(logging.Formatter( + '%(asctime)s | %(message)s' + )) + self.connection_logger.addHandler(connection_handler) + + # Performance logger (metrics over time) + self.performance_logger = logging.getLogger('performance') + self.performance_logger.setLevel(logging.INFO) + performance_handler = logging.FileHandler(self.performance_log) + performance_handler.setFormatter(logging.Formatter( + '%(asctime)s | %(message)s' + )) + self.performance_logger.addHandler(performance_handler) + + # Debug logger (technical details) + self.debug_logger = logging.getLogger('debug') + self.debug_logger.setLevel(logging.DEBUG) + debug_handler = logging.FileHandler(self.debug_log) + debug_handler.setFormatter(logging.Formatter( + '%(asctime)s | %(levelname)s | %(funcName)s:%(lineno)d | %(message)s' + )) + self.debug_logger.addHandler(debug_handler) + + def log_analysis_start(self, interface: str): + """Log the start of a new analysis session""" + self.analysis_logger.info("="*80) + self.analysis_logger.info(f"WiFi Mesh Network Analysis Session Started") + self.analysis_logger.info(f"Interface: {interface}") + self.analysis_logger.info(f"Session ID: {self.session_timestamp}") + self.analysis_logger.info("="*80) + self.debug_logger.info(f"Analysis session started on interface {interface}") + + def log_network_scan(self, aps_found: int, scan_duration: float): + """Log network scan results""" + self.analysis_logger.info(f"Network Scan Complete: {aps_found} APs found in {scan_duration:.2f}s") + self.debug_logger.info(f"Scan duration: {scan_duration:.3f}s, APs discovered: {aps_found}") + + def log_mesh_analysis(self, mesh_data: Dict): + """Log detailed mesh analysis results""" + self.analysis_logger.info("MESH TOPOLOGY ANALYSIS:") + self.analysis_logger.info(f" Brand: {mesh_data.get('brand', 'Unknown')}") + self.analysis_logger.info(f" Type: {mesh_data.get('mesh_type', 'Unknown')}") + self.analysis_logger.info(f" Nodes: {mesh_data.get('total_nodes', 0)}") + self.analysis_logger.info(f" Radios: {mesh_data.get('total_radios', 0)}") + self.analysis_logger.info(f" Bands: {', '.join(mesh_data.get('bands', []))}") + self.analysis_logger.info(f" Topology Health: {mesh_data.get('topology_health', 'Unknown')}") + self.analysis_logger.info(f" Signal Range: {mesh_data.get('signal_range', 0)}dB") + + # Log detailed node information + mesh_nodes = mesh_data.get('mesh_nodes', {}) + for node_id, node_info in mesh_nodes.items(): + self.analysis_logger.info(f" Node {node_id}: {node_info['strongest_signal']}dBm, {len(node_info['radios'])} radios") + for radio in node_info['radios']: + self.analysis_logger.info(f" Radio {radio['bssid']}: {radio['signal']}dBm ({radio['band']})") + + # Store as JSON for structured analysis with proper serialization + try: + serializable_data = make_json_serializable(mesh_data) + self.debug_logger.info(f"Mesh topology data: {json.dumps(serializable_data, indent=2)}") + except Exception as e: + self.debug_logger.warning(f"Could not serialize mesh data for JSON logging: {e}") + self.debug_logger.info(f"Mesh topology data (raw): {mesh_data}") + + def log_connection_event(self, event): + """Log WiFi connection events""" + event_msg = f"EVENT: {event.event_type.upper()} | BSSID: {event.bssid} | Signal: {event.signal}dBm" + if event.duration: + event_msg += f" | Duration: {event.duration:.1f}s" + if event.reason: + event_msg += f" | Reason: {event.reason}" + + self.connection_logger.info(event_msg) + self.debug_logger.debug(f"Connection event: {event}") + + def log_performance_metrics(self, current_conn: Dict, alternatives: List[Dict]): + """Log performance metrics and recommendations""" + if current_conn: + self.performance_logger.info(f"CURRENT CONNECTION:") + self.performance_logger.info(f" BSSID: {current_conn['bssid']}") + self.performance_logger.info(f" Signal: {current_conn['signal']}dBm") + self.performance_logger.info(f" Frequency: {current_conn['freq']}MHz") + self.performance_logger.info(f" Band: {self._get_band_from_freq(current_conn['freq'])}") + + if alternatives: + self.performance_logger.info(f"ALTERNATIVES FOUND: {len(alternatives)}") + for i, alt in enumerate(alternatives[:3], 1): + band = self._get_band_from_freq(alt['freq']) + self.performance_logger.info( + f" Option {i}: {alt['bssid']} | {alt['signal']}dBm ({band}) | " + f"Score: {alt['score']:.1f} | Diff: {alt['signal_diff']:+d}dB" + ) + + def log_recommendations(self, recommendations: Dict): + """Log analysis recommendations""" + self.analysis_logger.info("RECOMMENDATIONS:") + if recommendations.get('action_recommended'): + self.analysis_logger.info(f" Action: {recommendations['action']}") + self.analysis_logger.info(f" Target: {recommendations.get('target_bssid', 'Unknown')}") + self.analysis_logger.info(f" Expected Improvement: {recommendations.get('signal_improvement', 0)}dB") + self.analysis_logger.info(f" Priority: {recommendations.get('priority', 'Unknown')}") + self.analysis_logger.info(f" Method: {recommendations.get('method', 'Unknown')}") + else: + self.analysis_logger.info(" No action recommended - current connection optimal") + + # Store detailed recommendations as JSON with proper serialization + try: + serializable_recommendations = make_json_serializable(recommendations) + self.debug_logger.info(f"Recommendations data: {json.dumps(serializable_recommendations, indent=2)}") + except Exception as e: + self.debug_logger.warning(f"Could not serialize recommendations for JSON logging: {e}") + self.debug_logger.info(f"Recommendations data (raw): {recommendations}") + + def log_problems_detected(self, patterns: Dict): + """Log detected problems and patterns""" + total_issues = sum(len(v) if isinstance(v, list) else len(v) if isinstance(v, dict) else 0 + for v in patterns.values()) + + self.analysis_logger.info(f"PROBLEM DETECTION: {total_issues} issues found") + + if patterns.get('roaming_loops'): + self.analysis_logger.info(f" Roaming Loops: {len(patterns['roaming_loops'])}") + if patterns.get('auth_failure_clusters'): + self.analysis_logger.info(f" Auth Failures: {len(patterns['auth_failure_clusters'])}") + if patterns.get('rapid_disconnects'): + self.analysis_logger.info(f" Rapid Disconnects: {len(patterns['rapid_disconnects'])}") + + if total_issues > 0: + try: + serializable_patterns = make_json_serializable(patterns) + self.debug_logger.warning(f"Problems detected: {json.dumps(serializable_patterns, indent=2)}") + except Exception as e: + self.debug_logger.warning(f"Could not serialize patterns for JSON logging: {e}") + self.debug_logger.warning(f"Problems detected (raw): {patterns}") + + def log_command_execution(self, command: str, output: str, duration: float): + """Log system command execution for debugging""" + self.debug_logger.debug(f"Command: {command}") + self.debug_logger.debug(f"Duration: {duration:.3f}s") + if len(output) > 1000: + self.debug_logger.debug(f"Output: {output[:500]}...[truncated]...{output[-500:]}") + else: + self.debug_logger.debug(f"Output: {output}") + + def log_error(self, error: Exception, context: str = ""): + """Log errors with context""" + error_msg = f"ERROR in {context}: {type(error).__name__}: {str(error)}" + self.analysis_logger.error(error_msg) + self.debug_logger.exception(f"Exception in {context}") + + def _get_band_from_freq(self, freq: int) -> str: + """Helper to get band name from frequency""" + if 2400 <= freq <= 2500: + return '2.4GHz' + elif 5000 <= freq <= 5999: + return '5GHz' + elif 6000 <= freq <= 7125: + return '6GHz' + else: + return f'{freq}MHz' + + def create_analysis_archive(self) -> str: + """Create compressed archive of all logs and data""" + try: + # Create archive filename + archive_name = f"mesh_analysis_{self.session_timestamp}.zip" + archive_path = self.data_dir / archive_name + + with zipfile.ZipFile(archive_path, 'w', zipfile.ZIP_DEFLATED) as zipf: + # Add all log files from today + for log_file in self.logs_dir.glob("*.log"): + if self.session_date in log_file.name or self.session_timestamp in log_file.name: + zipf.write(log_file, f"logs/{log_file.name}") + + # Add data files + data_files = [ + self.data_dir / "bssid_history.pkl", + self.data_dir / "connection_events.pkl" + ] + + for data_file in data_files: + if data_file.exists(): + zipf.write(data_file, f"data/{data_file.name}") + + # Create summary file + summary_content = self._create_session_summary() + zipf.writestr("session_summary.txt", summary_content) + + # Create README + readme_content = self._create_readme() + zipf.writestr("README.txt", readme_content) + + self.analysis_logger.info(f"Analysis archive created: {archive_path}") + return str(archive_path) + + except Exception as e: + self.log_error(e, "create_analysis_archive") + return "" + + def _create_session_summary(self) -> str: + """Create a summary of the analysis session""" + summary = [] + summary.append("WiFi Mesh Network Analysis Session Summary") + summary.append("=" * 50) + summary.append(f"Session ID: {self.session_timestamp}") + summary.append(f"Date: {self.session_date}") + summary.append(f"Logs Directory: {self.logs_dir}") + summary.append("") + summary.append("Files Included:") + summary.append("- logs/analysis_*.log - Detailed analysis results") + summary.append("- logs/connections_*.log - WiFi connection events") + summary.append("- logs/performance_*.log - Performance metrics") + summary.append("- logs/debug_*.log - Technical debugging info") + summary.append("- data/bssid_history.pkl - Historical BSSID performance data") + summary.append("- data/connection_events.pkl - Connection event history") + summary.append("") + summary.append("Analysis Tools:") + summary.append("- Mesh topology detection and health assessment") + summary.append("- Signal strength analysis and optimization") + summary.append("- Historical performance tracking") + summary.append("- Problem pattern detection") + summary.append("- Actionable recommendations with step-by-step guidance") + + return "\n".join(summary) + + def _create_readme(self) -> str: + """Create README for the archive""" + readme = [] + readme.append("WiFi Mesh Network Analyzer - Log Archive") + readme.append("=" * 45) + readme.append("") + readme.append("This archive contains comprehensive logs from a WiFi mesh network analysis session.") + readme.append("") + readme.append("LOG FILES:") + readme.append("") + readme.append("analysis_*.log") + readme.append(" - Complete analysis results including mesh topology, recommendations,") + readme.append(" and problem detection. Human-readable format.") + readme.append("") + readme.append("connections_*.log") + readme.append(" - Real-time WiFi connection events including roaming, disconnects,") + readme.append(" and authentication events. Useful for troubleshooting connectivity issues.") + readme.append("") + readme.append("performance_*.log") + readme.append(" - Performance metrics over time including signal strength variations,") + readme.append(" alternative options, and optimization opportunities.") + readme.append("") + readme.append("debug_*.log") + readme.append(" - Technical debugging information including command outputs,") + readme.append(" timing data, and detailed system interactions.") + readme.append("") + readme.append("DATA FILES:") + readme.append("") + readme.append("data/bssid_history.pkl") + readme.append(" - Binary file containing historical performance data for each BSSID") + readme.append(" - Includes stability scores, connection success rates, signal tracking") + readme.append("") + readme.append("data/connection_events.pkl") + readme.append(" - Binary file containing detailed connection event history") + readme.append(" - Used for pattern analysis and problem detection") + readme.append("") + readme.append("USAGE:") + readme.append("These logs can be used for:") + readme.append("- Network troubleshooting and optimization") + readme.append("- Performance trend analysis") + readme.append("- Mesh system health monitoring") + readme.append("- Problem pattern identification") + readme.append("- Technical support and debugging") + + return "\n".join(readme) + +class HistoryTracker: + """Tracks WiFi connection history and performance per BSSID""" + + def __init__(self, data_dir: str = None, log_manager = None): + if data_dir is None: + # Use consistent location regardless of sudo + if os.geteuid() == 0 and 'SUDO_USER' in os.environ: + sudo_user = os.environ['SUDO_USER'] + import pwd + real_user_home = pwd.getpwnam(sudo_user).pw_dir + data_dir = os.path.join(real_user_home, ".mesh_analyzer") + else: + home = os.path.expanduser("~") + data_dir = os.path.join(home, ".mesh_analyzer") + + self.data_dir = Path(data_dir) + self.log_manager = log_manager + + # Create directory with proper permissions + try: + self.data_dir.mkdir(parents=True, exist_ok=True) + + # If created as root but should belong to user, fix permissions + if os.geteuid() == 0 and 'SUDO_USER' in os.environ: + sudo_user = os.environ['SUDO_USER'] + import pwd + user_info = pwd.getpwnam(sudo_user) + os.chown(self.data_dir, user_info.pw_uid, user_info.pw_gid) + + except Exception as e: + print(f"⚠️ Warning: Could not create/fix permissions for {data_dir}: {e}") + if self.log_manager: + self.log_manager.log_error(e, "HistoryTracker.__init__") + + self.history_file = self.data_dir / "bssid_history.pkl" + self.events_file = self.data_dir / "connection_events.pkl" + + self.bssid_history: Dict[str, BSSIDHistory] = {} + self.connection_events: List[ConnectionEvent] = [] + + self._load_history() + print(f"📁 History storage: {self.data_dir}") + + def _load_history(self): + """Load historical data from disk""" + try: + if self.history_file.exists(): + with open(self.history_file, 'rb') as f: + self.bssid_history = pickle.load(f) + print(f"📊 Loaded history for {len(self.bssid_history)} BSSIDs") + if self.log_manager: + self.log_manager.debug_logger.info(f"Loaded BSSID history: {len(self.bssid_history)} entries") + + if self.events_file.exists(): + with open(self.events_file, 'rb') as f: + self.connection_events = pickle.load(f) + # Keep only last 30 days of events + cutoff = time.time() - (30 * 24 * 3600) + original_count = len(self.connection_events) + self.connection_events = [e for e in self.connection_events if e.timestamp > cutoff] + cleaned_count = original_count - len(self.connection_events) + if cleaned_count > 0: + print(f"🧹 Cleaned {cleaned_count} old events (keeping last 30 days)") + if self.log_manager: + self.log_manager.debug_logger.info(f"Cleaned {cleaned_count} old events, keeping {len(self.connection_events)}") + print(f"📈 Loaded {len(self.connection_events)} recent connection events") + except (pickle.UnpicklingError, EOFError, pickle.PickleError) as e: + print(f"⚠️ Could not load history (corrupted pickle files): {e}") + print("🔧 Attempting to recover by backing up and resetting history...") + if self.log_manager: + self.log_manager.log_error(e, "load_history") + self._backup_and_reset_corrupted_files() + self.bssid_history = {} + self.connection_events = [] + print("🆕 Starting with fresh history tracking") + except Exception as e: + print(f"⚠️ Could not load history: {e}") + print("🆕 Starting with fresh history tracking") + if self.log_manager: + self.log_manager.log_error(e, "load_history") + self.bssid_history = {} + self.connection_events = [] + + def _backup_and_reset_corrupted_files(self): + """Backup corrupted files and reset for fresh start""" + try: + import shutil + timestamp = int(time.time()) + + if self.history_file.exists(): + backup_history = self.data_dir / f"bssid_history_corrupted_{timestamp}.pkl" + shutil.move(str(self.history_file), str(backup_history)) + print(f"📦 Backed up corrupted history to: {backup_history.name}") + + if self.events_file.exists(): + backup_events = self.data_dir / f"connection_events_corrupted_{timestamp}.pkl" + shutil.move(str(self.events_file), str(backup_events)) + print(f"📦 Backed up corrupted events to: {backup_events.name}") + + except Exception as e: + print(f"⚠️ Could not backup corrupted files: {e}") + + def _save_history(self): + """Save historical data to disk""" + try: + self.data_dir.mkdir(exist_ok=True) + + with open(self.history_file, 'wb') as f: + pickle.dump(self.bssid_history, f) + + with open(self.events_file, 'wb') as f: + pickle.dump(self.connection_events, f) + + # Fix file permissions if running as sudo + if os.geteuid() == 0 and 'SUDO_USER' in os.environ: + sudo_user = os.environ['SUDO_USER'] + import pwd + user_info = pwd.getpwnam(sudo_user) + os.chown(self.history_file, user_info.pw_uid, user_info.pw_gid) + os.chown(self.events_file, user_info.pw_uid, user_info.pw_gid) + + except Exception as e: + print(f"⚠️ Warning: Could not save history: {e}") + + def record_event(self, event: ConnectionEvent): + """Record a connection event""" + self.connection_events.append(event) + + # Log the event + if self.log_manager: + self.log_manager.log_connection_event(event) + + # Update BSSID history + if event.bssid not in self.bssid_history: + self.bssid_history[event.bssid] = BSSIDHistory(bssid=event.bssid) + + history = self.bssid_history[event.bssid] + history.last_seen = event.timestamp + + if event.event_type == 'connect': + history.total_connections += 1 + history.successful_connections += 1 + elif event.event_type == 'auth_timeout': + history.auth_failures += 1 + elif event.event_type == 'disconnect': + history.disconnects += 1 + if event.duration: + history.total_duration += event.duration + + # Update signal tracking + if event.signal != -100: + history.signal_samples.append((event.timestamp, event.signal)) + history.signal_samples = history.signal_samples[-100:] # Keep last 100 + + if history.signal_samples: + history.avg_signal = sum(s[1] for s in history.signal_samples) / len(history.signal_samples) + + # Calculate stability score + self._calculate_stability_score(history) + self._save_history() + + def _calculate_stability_score(self, history: BSSIDHistory): + """Calculate stability score (0-100) based on historical performance""" + score = 100.0 + + # Penalize auth failures (50 point penalty max) + if history.total_connections > 0: + failure_rate = history.auth_failures / (history.total_connections + history.auth_failures) + score -= failure_rate * 50 + + # Penalize frequent disconnects (30 point penalty max) + if history.successful_connections > 0: + disconnect_rate = history.disconnects / history.successful_connections + score -= min(disconnect_rate * 30, 30) + + # Reward consistent signal (10 point bonus for consistency) + if len(history.signal_samples) > 5: + signals = [s[1] for s in history.signal_samples[-10:]] + signal_variance = max(signals) - min(signals) + if signal_variance < 10: + score += 10 + elif signal_variance > 30: + score -= 20 + + # Reward longer connection durations + if history.successful_connections > 0 and history.total_duration > 0: + avg_duration = history.total_duration / history.successful_connections + if avg_duration > 3600: # > 1 hour + score += 15 + elif avg_duration < 300: # < 5 minutes + score -= 15 + + history.stability_score = max(0, min(100, score)) + + def get_bssid_performance(self, bssid: str) -> Optional[BSSIDHistory]: + """Get historical performance for a specific BSSID""" + return self.bssid_history.get(bssid) + + def get_recent_events(self, hours: int = 24) -> List[ConnectionEvent]: + """Get connection events from the last N hours""" + cutoff = time.time() - (hours * 3600) + return [e for e in self.connection_events if e.timestamp > cutoff] + +class MeshIntelligence: + """Mesh network topology analysis with built-in OUI database""" + + def __init__(self): + # Updated mesh brand OUI database (May 2025) + self.oui_database = { + # eero (Amazon) - Mesh WiFi Systems + 'eero': [ + 'A0:21:B7', '68:1D:A0', 'B0:8E:86', 'F8:BB:BF', 'D8:8E:D4', 'E8:D3:EB', # Original entries + '00:AB:48', '80:DA:13', '74:B6:B6', '6C:AE:F6', '68:4A:76', '60:5F:8D', # Additional eero prefixes + '50:F5:DA', 'C4:93:D9', '58:D9:D5', '50:1A:C5', '04:D3:B0', '24:F5:AA', + '9C:30:5B', 'A8:81:95', '74:75:48', '60:32:B1', '84:D8:1B', '00:90:4C', + '70:56:81', 'C8:69:CD', '40:B4:CD', 'BC:E6:43', '8C:85:90', 'DC:A6:32', + '88:E9:FE', '28:6C:07', '3C:22:FB', '90:72:40', 'D0:04:01', 'AC:BC:32', + '34:D2:70', 'B0:BE:76', '58:8B:F3', 'EC:01:EE', 'A4:11:6B', '70:4D:7B', + '98:F1:70', 'CC:32:E5', '40:A3:CC', '1C:69:7A', 'B8:C7:5D', '2C:1F:23', + '44:CE:7D', 'D4:61:9D', '78:4F:43', '0C:47:C9', 'B4:A9:FC', '88:1F:A1', + 'FC:EC:DA', '30:23:03', '24:A0:74', '6C:72:20', 'E0:55:3D', '48:43:7C' + ], + + # Netgear Orbi - Mesh WiFi Systems + 'orbi_netgear': [ + '98:97:9A', '44:B1:3B', '9C:28:EF', 'A0:04:60', '04:A1:51', # Original entries + '10:0D:7F', '28:C6:8E', 'B0:7F:B9', '4C:60:DE', '9C:3D:CF', # Additional Netgear prefixes + 'A0:40:A0', '20:E5:2A', 'C4:04:15', '84:1B:5E', '40:16:7E', + '2C:30:33', 'E0:46:9A', '6C:19:8F', 'C0:3F:0E', '08:BD:43', + '74:44:01', 'B0:39:56', '30:46:9A', 'A0:63:91', '44:94:FC', + '3C:37:86', 'R0:48:7A', '1C:C1:DE', '78:D2:94', 'DC:EF:09', + '08:02:8E', '74:98:0B', 'A4:2B:B0', '50:C7:BF', '6C:B0:CE', + '84:A4:23', 'E0:91:F5', 'CC:40:D0', '9C:5C:8E', '28:56:5A', + '70:4F:57', 'FC:94:E3', '1C:BD:B9', 'B4:75:0E', '34:98:B5', + '40:0D:10', '6C:CD:D6', 'A0:21:B7', '30:87:30', '50:6A:03' + ], + + # Google Nest WiFi & Google WiFi + 'google_nest': [ + 'A4:50:46', '64:FF:89', 'CC:52:AF', '6C:71:0D', # Original entries + 'F4:F5:D8', '4C:49:E3', '78:E1:03', '18:B4:30', # Additional Google prefixes + '30:FD:38', 'A4:DA:32', '90:72:40', 'F8:8F:CA', + '6C:AD:F8', 'F0:EF:86', '40:4E:36', 'E8:40:F2', + 'B4:CE:F6', '84:F3:EB', '3C:36:3D', '00:1A:11', + 'D8:50:E6', 'B0:79:94', 'C8:14:79', '54:60:09', + '68:C6:3A', 'DC:3A:5E', '48:57:02', '7C:2E:BD', + '98:DE:D0', '14:2D:27', 'B8:AD:28', 'E0:CB:4E', + '20:DF:B9', 'A0:C5:89', '74:E5:43', '58:CB:52', + '88:3F:D3', 'C4:B3:01', '60:F1:89', '9C:B6:D0' + ], + + # ASUS - WiFi Routers and Mesh Systems + 'asus': [ + '40:ED:00', '88:1F:A1', 'AC:9E:17', '2C:56:DC', '04:D4:C4', # Original entries + '70:4D:7B', 'B8:EE:65', '1C:87:2C', '50:46:5D', 'D8:50:E6', # Additional ASUS prefixes + '38:D5:47', 'F0:2F:74', '30:5A:3A', '04:92:26', '00:E0:4C', + '00:08:A1', '00:0E:A6', '00:11:D8', '00:13:D4', '00:15:F2', + '00:17:31', '00:19:DB', '00:1B:FC', '00:1E:8C', '00:22:15', + '00:23:54', '00:24:8C', '00:26:18', '08:60:6E', '10:7B:44', + '14:DD:A9', '20:CF:30', '24:4B:FE', '28:10:7B', '30:85:A9', + '34:97:F6', '38:2C:4A', '3C:7C:3F', '40:16:7E', '48:EE:0C', + '4C:ED:FB', '50:3E:AA', '54:04:A6', '5C:AC:4C', '60:45:CB', + '64:66:B3', '6C:F0:49', '70:8B:CD', '74:D0:2B', '78:24:AF', + '7C:10:C9', '80:1F:02', '84:A4:23', '88:D7:F6', '8C:10:D4', + '90:F6:52', '94:FB:A7', '98:5A:EB', '9C:5C:8E', 'A0:F3:C1', + 'AC:22:0B', 'B0:6E:BF', 'B4:2E:99', 'B8:AE:6E', 'BC:EE:7B', + 'C8:60:00', 'CC:2D:E0', 'D0:17:C2', 'D4:5D:64', 'D8:47:32', + 'DC:FB:02', 'E0:3F:49', 'E4:70:B8', 'E8:CC:18', 'EC:F4:BB', + 'F0:79:59', 'F4:6D:04', 'F8:32:E4', 'FC:34:97' + ], + + # TP-Link Deco Mesh Systems + 'tp_link_deco': [ + '98:25:4A', '44:94:FC', 'B0:48:7A', '50:C7:BF', 'A4:2B:B0', # Original entries + '14:CC:20', '1C:61:B4', '98:48:27', 'A4:2B:B0', '18:A6:F7', # Additional TP-Link prefixes + '00:23:CD', '00:27:19', '04:8D:38', '08:55:31', '0C:80:63', + '10:27:F5', '14:E6:E4', '18:D6:C7', '1C:FA:68', '20:F4:78', + '24:05:0F', '28:2C:02', '2C:F0:5D', '30:07:4D', '34:29:8F', + '38:71:DE', '3C:84:6A', '40:A5:EF', '44:D9:E7', '48:3B:38', + '4C:E1:73', '50:1A:C5', '54:AF:97', '58:8E:81', '5C:62:8B', + '60:E3:27', '64:70:02', '68:FF:7B', '6C:5A:B0', '70:4F:57', + '74:DA:38', '78:81:02', '7C:8A:E1', '80:EA:96', '84:16:F9', + '88:C3:97', '8C:53:C3', '90:F6:52', '94:E9:79', '98:DA:C4', + '9C:A6:15', 'A0:F3:C1', 'A4:B1:E9', 'A8:40:41', 'AC:84:C6', + 'B0:4E:26', 'B4:B0:24', 'B8:69:F4', 'BC:46:99', 'C0:06:C3', + 'C4:6E:1F', 'C8:0E:14', 'CC:32:E5', 'D0:76:E7', 'D4:6E:0E', + 'D8:0D:17', 'DC:9F:DB', 'E0:28:6D', 'E4:9A:DC', 'E8:DE:27', + 'EC:08:6B', 'F0:F2:49', 'F4:28:53', 'F8:1A:67', 'FC:7C:02' + ], + + # Linksys Velop Mesh Systems + 'linksys_velop': [ + '6C:BE:E9', '13:10:47', '98:9E:64', '94:10:3E', 'C4:41:1E', # Original entries + '00:0F:66', '00:13:10', '00:14:BF', '00:16:B6', '00:18:39', # Additional Linksys/Cisco prefixes + '00:1A:70', '00:1C:10', '00:1D:7E', '00:1E:E5', '00:21:29', + '00:22:6B', '00:23:04', '00:24:13', '00:25:45', '00:40:96', + '08:86:3B', '10:05:CA', '14:91:82', '18:1B:EB', '1C:DF:0F', + '20:AA:4B', '24:F2:7F', '28:F0:76', '2C:AB:A4', '30:23:03', + '34:A8:4E', '38:2A:68', '3C:1E:04', '40:B0:FA', '44:32:C8', + '48:F8:B3', '4C:00:82', '50:3D:E5', '54:78:1A', '58:6D:8F', + '5C:50:15', '60:38:E0', '64:1C:B0', '68:7F:74', '6C:50:4D', + '70:1A:04', '74:E2:F5', '78:CA:39', '7C:34:79', '80:69:1A', + '84:B5:17', '88:CB:87', '8C:04:BA', '90:35:5B', '94:44:52', + '98:FC:11', '9C:97:26', 'A0:55:4F', 'A4:18:75', 'A8:9C:ED', + 'AC:1D:DF', 'B0:10:41', 'B4:3A:28', 'B8:55:10', 'BC:67:78', + 'C0:56:27', 'C4:7C:8D', 'C8:D7:19', 'CC:5D:4E', 'D0:59:E4', + 'D4:CA:6D', 'D8:FE:E3', 'DC:85:DE', 'E0:1C:41', 'E4:F4:C6', + 'E8:98:6D', 'EC:E1:A9', 'F0:92:1C', 'F4:EC:38', 'F8:E7:1E' + ], + + # Ubiquiti Networks - UniFi, AmpliFi, EdgeRouter + 'ubiquiti': [ + '78:8A:20', '24:5A:4C', 'F0:9F:C2', '44:D9:E7', 'E0:63:DA', # Original entries + '04:18:D6', '68:72:51', 'B4:FB:E4', 'DC:9F:DB', '80:2A:A8', # Additional Ubiquiti prefixes + '00:15:6D', '00:27:22', '04:18:D6', '18:E8:29', '24:A4:3C', + '44:D9:E7', '68:72:51', '6C:88:14', '74:83:C2', '78:8A:20', + '80:2A:A8', 'B4:FB:E4', 'DC:9F:DB', 'E0:63:DA', 'F0:9F:C2', + '24:5A:4C', '68:D7:9A', '70:A7:41', '74:AC:B9', '78:45:58', + '80:2A:A8', '84:B5:9C', '88:1F:A1', '8C:59:C3', '90:9A:4A', + '94:3E:EA', '98:FA:9B', '9C:93:4E', 'A0:F3:E4', 'A4:2B:8C', + 'A8:40:25', 'AC:8B:A9', 'B0:C5:54', 'B4:E6:2D', 'B8:27:EB', + 'BC:DD:C2', 'C0:4A:00', 'C4:A8:1D', 'C8:7F:54', 'CC:2D:A0', + 'D0:21:F9', 'D4:CA:6D', 'D8:B3:70', 'DC:2C:26', 'E0:22:F0', + 'E4:38:7E', 'E8:CC:18', 'EC:B9:70', 'F0:27:2D', 'F4:92:BF', + 'F8:AB:05', 'FC:EC:DA' + ], + + # MikroTik RouterOS Devices + 'mikrotik': [ + '6C:3B:6B', '48:8F:5A', '2C:C8:1B', '4C:5E:0C', 'E4:8D:8C', # Original entries + '00:0C:42', '18:FD:74', '2C:C8:1B', '4C:5E:0C', '6C:3B:6B', # Additional MikroTik prefixes + '74:4D:28', '7C:2F:80', '8C:59:C3', 'B8:69:F4', 'DC:2C:6E', + 'E4:8D:8C', '48:8F:5A', '08:55:31', '18:FD:74', '00:0C:42', + '6C:3B:6B', '4C:5E:0C', 'E4:8D:8C', '2C:C8:1B', '48:8F:5A', + 'B8:69:F4', 'DC:2C:6E', '74:4D:28', '8C:59:C3', '18:FD:74', + '7C:2F:80', '00:0C:42', '08:55:31', '84:1B:5E', '90:5A:68', + '94:E3:6D', '98:DA:C4', '9C:A6:15', 'A0:F3:C1', 'A4:B1:E9', + 'A8:40:41', 'AC:84:C6', 'B0:4E:26', 'B4:B0:24', 'B8:69:F4', + 'BC:46:99', 'C0:06:C3', 'C4:6E:1F', 'C8:0E:14', 'CC:32:E5', + 'D0:76:E7', 'D4:6E:0E', 'D8:0D:17', 'DC:9F:DB' + ], + + # Aruba HPE Networks + 'aruba_hpe': [ + '70:3A:CB', '6C:F3:7F', '24:DE:C6', '94:B4:0F', '20:4C:03', # Original entries + '00:0B:86', '00:1A:1E', '00:24:6C', '6C:F3:7F', '70:3A:CB', # Additional Aruba/HPE prefixes + '78:9C:85', '84:D4:7E', '8C:DC:D4', '94:B4:0F', '9C:1C:12', + 'A4:5D:36', 'B0:5A:DA', 'B8:D9:CE', 'C0:E4:34', 'D8:C7:C8', + 'E0:07:1B', 'E8:BA:70', 'F0:5C:19', 'F8:0A:CB', '00:0B:86', + '00:1A:1E', '00:24:6C', '18:64:72', '20:4C:03', '24:DE:C6', + '40:E3:D6', '54:75:D0', '6C:C2:17', '70:3A:CB', '7C:69:F6', + '84:D4:7E', '8C:DC:D4', '94:B4:0F', '9C:1C:12', 'A4:5D:36', + 'B0:5A:DA', 'B8:D9:CE', 'C0:E4:34', 'D8:C7:C8', 'E0:07:1B', + 'E8:BA:70', 'F0:5C:19', 'F8:0A:CB', '6C:F3:7F', '20:4C:03' + ], + + # Ruckus Networks (CommScope) + 'ruckus': [ + '50:91:E3', '2C:36:F8', '94:3E:EA', 'BC:14:85', '58:93:96', # Original entries + '2C:36:F8', '50:91:E3', '58:93:96', '94:3E:EA', 'BC:14:85', # Additional Ruckus prefixes + 'C4:B9:CD', 'E0:5F:B9', 'F4:28:53', '00:0F:9F', '00:14:7F', + '00:21:91', '00:24:DC', '78:BC:1A', '84:1B:5E', '90:5A:68', + '2C:36:F8', '50:91:E3', '58:93:96', '94:3E:EA', 'BC:14:85', + 'C4:B9:CD', 'E0:5F:B9', 'F4:28:53', '00:0F:9F', '00:14:7F', + '00:21:91', '00:24:DC', '78:BC:1A', '84:1B:5E', '90:5A:68', + '94:E3:6D', '98:DA:C4', '9C:A6:15', 'A0:F3:C1', 'A4:B1:E9', + 'A8:40:41', 'AC:84:C6', 'B0:4E:26', 'B4:B0:24', 'BC:46:99' + ], + + # Cisco Meraki Cloud Managed Networks + 'cisco_meraki': [ + '00:18:0A', 'E0:55:3D', '88:15:44', '0C:8D:DB', '34:56:FE', # Original entries + '00:18:0A', '0C:8D:DB', '34:56:FE', '88:15:44', 'E0:55:3D', # Additional Cisco Meraki prefixes + '00:1D:71', '00:24:DC', 'E0:CB:BC', 'F4:39:09', '58:97:1E', + '8C:7C:92', 'AC:17:C8', 'E0:CB:BC', 'F4:39:09', '58:97:1E', + '00:1D:71', '00:24:DC', '8C:7C:92', 'AC:17:C8', '00:18:0A', + '0C:8D:DB', '34:56:FE', '88:15:44', 'E0:55:3D', 'E0:CB:BC', + 'F4:39:09', '58:97:1E', '8C:7C:92', 'AC:17:C8', '00:1D:71', + '00:24:DC', '2C:BE:08', '4C:79:6E', '74:86:E2', '8C:FE:A3', + 'A4:56:02', 'BC:67:1C', 'D4:20:B0', 'EC:1F:72', '2C:BE:08', + '4C:79:6E', '74:86:E2', '8C:FE:A3', 'A4:56:02', 'BC:67:1C' + ], + + # EnGenius Wireless Access Points + 'engenius': [ + '88:DC:96', '50:2B:73', '02:CF:7F', '00:02:6F', # Original entries + '00:02:6F', '02:CF:7F', '50:2B:73', '88:DC:96', # Additional EnGenius prefixes + '00:02:6F', '88:DC:96', '50:2B:73', '02:CF:7F', + '74:EA:3A', 'AC:9E:17', 'C8:D3:A3', 'DC:EF:09', + 'F0:7D:68', '74:EA:3A', 'AC:9E:17', 'C8:D3:A3', + 'DC:EF:09', 'F0:7D:68', '88:DC:96', '50:2B:73', + '02:CF:7F', '00:02:6F', '74:EA:3A', 'AC:9E:17', + 'C8:D3:A3', 'DC:EF:09', 'F0:7D:68', '04:F0:21', + '6C:72:20', '88:6B:0E', 'AC:83:F3', 'D0:17:C2', + 'F4:AF:E7', '04:F0:21', '6C:72:20', '88:6B:0E' + ], + + # D-Link WiFi Routers and Access Points + 'dlink': [ + 'CC:B2:55', 'B8:A3:86', '34:08:04', '14:D6:4D', '84:C9:B2', # Original entries + '00:05:5D', '00:0F:3D', '00:11:95', '00:13:46', '00:15:E9', # Additional D-Link prefixes + '00:17:9A', '00:19:5B', '00:1B:11', '00:1C:F0', '00:1E:58', + '00:21:91', '00:22:B0', '00:24:01', '00:26:5A', '14:D6:4D', + '1C:7E:E5', '1C:AF:F7', '28:10:7B', '2C:B0:5D', '34:08:04', + '40:61:86', '48:EE:0C', '50:C7:BF', '54:78:1A', '5C:F4:AB', + '60:C5:47', '6C:19:8F', '70:62:B8', '78:54:2E', '7C:8B:CA', + '84:C9:B2', '8C:BE:BE', '90:94:E4', '94:44:52', '9C:D6:43', + 'A0:AB:1B', 'A8:57:4E', 'B0:C7:45', 'B8:A3:86', 'C0:A0:BB', + 'C8:BE:19', 'CC:B2:55', 'D0:67:E5', 'D8:FE:E3', 'E0:91:F5', + 'E8:CC:18', 'F0:7D:68', 'F8:E7:1E', 'FC:75:16' + ], + + # Netgear General (Non-Orbi) Products + 'netgear_general': [ + '10:0D:7F', '28:C6:8E', 'B0:7F:B9', '4C:60:DE', # Original entries + '00:09:5B', '00:0F:B5', '00:14:6C', '00:1B:2F', '00:1E:2A', + '00:22:3F', '00:24:B2', '00:26:F2', '04:A1:51', '08:BD:43', + '10:0D:7F', '20:4E:7F', '28:C6:8E', '2C:30:33', '30:46:9A', + '44:94:FC', '4C:60:DE', '6C:B0:CE', '70:4F:57', '74:44:01', + '78:D2:94', '84:A4:23', '9C:3D:CF', 'A0:04:60', 'A0:21:B7', + 'A0:63:91', 'A4:2B:B0', 'B0:39:56', 'B0:7F:B9', 'C0:3F:0E', + 'C4:04:15', 'CC:40:D0', 'DC:EF:09', 'E0:46:9A', 'E0:91:F5', + 'FC:94:E3', '1C:BD:B9', '1C:C1:DE', '3C:37:86', '40:0D:10', + '50:6A:03', '6C:CD:D6', '9C:5C:8E', 'B4:75:0E', '34:98:B5' + ], + + # Plume Adaptive WiFi (Plume Design) + 'plume_adaptive': [ + '74:DA:88', '78:28:CA', 'A0:40:A0', # Original entries + '74:DA:88', '78:28:CA', 'A0:40:A0', # Additional Plume prefixes + 'B8:D7:AF', 'C4:93:D9', 'E0:1C:FC', + '24:F5:AA', '58:D9:D5', '9C:30:5B', + 'A8:81:95', 'C0:C9:E3', 'E8:6A:64', + '04:D3:B0', '50:1A:C5', 'B0:BE:76', + 'EC:01:EE', '74:DA:88', '78:28:CA', + 'A0:40:A0', 'B8:D7:AF', 'C4:93:D9' + ], + + # Xfinity Pods (Comcast) + 'xfinity_pods': [ + 'A8:4E:3F', '00:35:1A', '8C:3B:AD', # Original entries + 'A8:4E:3F', '00:35:1A', '8C:3B:AD', + '70:56:81', 'C8:69:CD', '40:B4:CD', + 'BC:E6:43', '8C:85:90', 'DC:A6:32', + '88:E9:FE', '28:6C:07', '3C:22:FB', + '90:72:40', 'D0:04:01', 'AC:BC:32', + '34:D2:70', 'A8:4E:3F', '00:35:1A', + '8C:3B:AD', '70:56:81', 'C8:69:CD' + ], + + # Amazon AmpliFi (acquired by Amazon) + 'amazon_amplifi': [ + '74:C6:3B', 'E4:95:6E', # Original entries + '74:C6:3B', 'E4:95:6E', # Additional AmpliFi prefixes + 'DC:9F:DB', '44:D9:E7', 'F0:9F:C2', + '24:5A:4C', '78:8A:20', 'E0:63:DA', + 'B4:FB:E4', '04:18:D6', '68:72:51', + '80:2A:A8', '74:C6:3B', 'E4:95:6E', + 'DC:9F:DB', '44:D9:E7', 'F0:9F:C2' + ], + + # Tenda WiFi Routers + 'tenda': [ + 'C8:3A:35', 'FC:7C:02', '98:DE:D0', # Original entries + 'C8:3A:35', 'FC:7C:02', '98:DE:D0', # Additional Tenda prefixes + '00:B0:0C', '74:25:8A', 'A4:2B:8C', + 'C8:3A:35', 'FC:7C:02', '98:DE:D0', + '00:B0:0C', '74:25:8A', 'A4:2B:8C', + 'E0:05:C6', 'F4:EC:38', '34:96:72', + '5C:CF:7F', 'B0:E5:ED', 'D4:6E:0E', + 'E8:DE:27', '10:BF:48', '50:BD:5F', + '8C:21:0A', 'C8:3A:35', 'FC:7C:02' + ], + + # Xiaomi Mi Router and Mesh + 'xiaomi_mesh': [ + '34:CE:00', '64:64:4A', 'F8:59:71', # Original entries + '34:CE:00', '64:64:4A', 'F8:59:71', # Additional Xiaomi prefixes + '50:8F:4C', '78:11:DC', 'A0:86:C6', + 'B0:E2:35', 'C4:0B:CB', 'D4:97:0B', + 'E8:AB:FA', 'F0:B4:29', 'F8:59:71', + '04:CF:8C', '14:75:90', '28:E3:1F', + '3C:BD:D8', '50:EC:50', '68:DF:DD', + '7C:1D:D9', '8C:53:C3', '98:FA:9B', + 'A4:DA:32', 'B8:70:F4', 'C8:FF:28', + 'DC:44:27', 'F0:B4:29', '34:CE:00' + ], + + # Honor/Huawei WiFi Routers + 'honor_huawei': [ + '00:E0:FC', '98:F4:28', 'A0:8C:FD', # Original entries + '00:E0:FC', '98:F4:28', 'A0:8C:FD', # Additional Huawei/Honor prefixes + '00:25:9E', '04:BD:88', '10:47:80', + '18:CF:5E', '20:76:93', '28:31:52', + '30:FC:68', '3C:FA:43', '44:00:10', + '4C:54:99', '54:25:EA', '5C:C9:D3', + '64:3E:8C', '6C:92:BF', '74:A7:22', + '7C:A7:B0', '84:A8:E4', '8C:34:FD', + '94:04:9C', '9C:28:EF', 'A4:C4:94', + 'AC:E2:15', 'B4:CD:27', 'BC:76:70', + 'C4:6A:B7', 'CC:E6:7F', 'D4:20:B0', + 'DC:D2:FC', 'E4:C7:22', 'EC:23:3D', + 'F4:4E:E3', 'FC:48:EF', '98:F4:28' + ], + + # WiFi 6E and WiFi 7 Manufacturers + 'wifi6e_wifi7_general': [ + # Various next-gen WiFi manufacturers + '70:4F:57', '6C:CD:D6', '30:87:30', '50:6A:03', '40:0D:10', + 'B4:75:0E', '34:98:B5', '1C:BD:B9', 'FC:94:E3', 'E0:91:F5', + '84:A4:23', 'A0:63:91', '70:4F:57', '6C:B0:CE', '44:94:FC', + '30:46:9A', '2C:30:33', 'E0:46:9A', '6C:19:8F', 'C0:3F:0E', + '08:BD:43', '74:44:01', 'B0:39:56', '20:E5:2A', 'C4:04:15', + '84:1B:5E', '40:16:7E', '9C:3D:CF', 'A0:40:A0', '10:0D:7F', + '28:C6:8E', 'B0:7F:B9', '4C:60:DE', 'DC:EF:09', 'CC:40:D0' + ], + + # Additional Mesh Router Brands + 'additional_mesh_brands': [ + # Portal WiFi + '68:A4:0E', '84:16:0C', 'A0:8C:FD', 'B4:2E:99', 'C8:D3:A3', + # Securifi Almond + 'F0:7D:68', '74:EA:3A', 'AC:9E:17', 'DC:EF:09', 'C8:D3:A3', + # Luma WiFi + '44:61:32', '70:B3:D5', '9C:65:F9', 'C0:14:FE', 'E4:A7:A0', + # Gryphon Router + '00:1E:C7', '2C:AB:A4', '58:6D:8F', '84:B5:17', 'B0:10:41', + # Samsung SmartThings WiFi + '28:6D:CD', '5C:0A:5B', '88:36:6C', 'B4:E6:2D', 'E0:91:F5', + # Norton Core Router + '00:50:56', '00:0C:29', '00:05:69', '00:1C:14', '00:50:56' + ], + + # Industrial and Enterprise Mesh + 'industrial_enterprise': [ + # Cambium Networks + '00:04:56', '00:80:A1', '58:C1:7A', '84:1B:5E', 'B8:59:9F', + # Cradlepoint + '00:30:44', '8C:0E:E3', 'A4:93:4C', 'C0:EE:40', 'E4:E4:AB', + # Peplink + '00:15:FF', '00:1C:B5', '30:D1:7E', '6C:3B:E5', 'A8:1E:84', + # SonicWall + '00:06:B1', '00:17:C5', '2C:8A:72', '78:D2:94', 'C0:EA:E4', + # Fortinet FortiGate + '00:09:0F', '70:4C:A5', '90:6C:AC', 'A0:1D:48', 'B8:EE:65' + ] +} + + def identify_mesh_brand(self, bssids: List[str]) -> Optional[str]: + """Identify mesh system brand from BSSIDs""" + for bssid in bssids: + oui = ':'.join(bssid.split(':')[:3]).upper() + for brand, ouis in self.oui_database.items(): + if oui in [o.upper() for o in ouis]: + return brand + return None + + def analyze_mesh_topology(self, same_ssid_aps: List[APScan]) -> Dict: + """Analyze network structure - mesh vs single AP/WAP with appropriate evaluation""" + if len(same_ssid_aps) <= 1: + # Single AP - use signal strength analysis + ap = same_ssid_aps[0] if same_ssid_aps else None + if ap: + if ap.signal > -50: + quality = "excellent" + quality_reason = f"Strong signal ({ap.signal}dBm) indicates good placement" + elif ap.signal > -60: + quality = "good" + quality_reason = f"Good signal strength ({ap.signal}dBm)" + elif ap.signal > -75: + quality = "fair" + quality_reason = f"Moderate signal ({ap.signal}dBm) - consider moving closer or improving placement" + else: + quality = "poor" + quality_reason = f"Weak signal ({ap.signal}dBm) - poor placement or too far from AP" + + return { + 'type': 'single_ap', + 'nodes': 1, + 'signal_quality': quality, + 'signal_reason': quality_reason, + 'signal_strength': ap.signal + } + else: + return {'type': 'single_ap', 'nodes': 0} + + # Multiple APs - determine if it's a mesh or multiple standalone APs + mesh_nodes = {} + standalone_aps = [] + + for ap in same_ssid_aps: + base_mac = ':'.join(ap.bssid.split(':')[:-1]) + + # Check if this looks like a mesh node (same base MAC with different radios) + if base_mac in mesh_nodes: + # This is another radio on the same mesh node + mesh_nodes[base_mac]['radios'].append({ + 'bssid': ap.bssid, + 'freq': ap.freq, + 'signal': ap.signal, + 'band': self._determine_band(ap.freq) + }) + mesh_nodes[base_mac]['bands'].add(self._determine_band(ap.freq)) + mesh_nodes[base_mac]['strongest_signal'] = max( + mesh_nodes[base_mac]['strongest_signal'], ap.signal + ) + else: + # Check if any existing mesh node has a similar base MAC (mesh detection) + is_mesh_node = False + for existing_base in mesh_nodes.keys(): + # If base MACs are very similar, likely same mesh system + if self._is_likely_same_mesh_system([base_mac, existing_base]): + is_mesh_node = True + break + + if is_mesh_node or len([a for a in same_ssid_aps if ':'.join(a.bssid.split(':')[:-1]) == base_mac]) > 1: + # This is a mesh node + mesh_nodes[base_mac] = { + 'base_mac': base_mac, + 'radios': [{ + 'bssid': ap.bssid, + 'freq': ap.freq, + 'signal': ap.signal, + 'band': self._determine_band(ap.freq) + }], + 'bands': {self._determine_band(ap.freq)}, + 'strongest_signal': ap.signal + } + else: + # This looks like a standalone AP + standalone_aps.append(ap) + + # If we have mesh nodes, analyze as mesh + if mesh_nodes: + return self._analyze_mesh_system(mesh_nodes, same_ssid_aps) + else: + # Multiple standalone APs with same SSID + return self._analyze_multiple_aps(same_ssid_aps) + + def _determine_band(self, freq: int) -> str: + """Determine frequency band from frequency""" + if 2400 <= freq <= 2500: + return '2.4GHz' + elif 5000 <= freq <= 5999: + return '5GHz' + elif 6000 <= freq <= 7125: + return '6GHz' + else: + return 'other' + + def _is_likely_same_mesh_system(self, base_macs: List[str]) -> bool: + """Check if base MACs likely belong to same mesh system""" + # Simple heuristic: if OUI matches and MACs are in sequence + if len(base_macs) < 2: + return False + + ouis = [':'.join(mac.split(':')[:3]) for mac in base_macs] + return len(set(ouis)) == 1 # Same manufacturer + + def _analyze_mesh_system(self, mesh_nodes: Dict, same_ssid_aps: List[APScan]) -> Dict: + """Analyze actual mesh system topology and health with proper spatial analysis""" + total_nodes = len(mesh_nodes) + brand = self.identify_mesh_brand([ap.bssid for ap in same_ssid_aps]) + + # Determine mesh type + all_bands = set() + for node in mesh_nodes.values(): + all_bands.update(node['bands']) + + if len(all_bands) >= 3: + mesh_type = 'tri_band' + elif len(all_bands) == 2: + mesh_type = 'dual_band' + else: + mesh_type = 'single_band' + + # SOPHISTICATED SPATIAL ANALYSIS + signals = [node['strongest_signal'] for node in mesh_nodes.values()] + sorted_signals = sorted(signals, reverse=True) + signal_range = max(signals) - min(signals) + + # Analyze signal distribution and coverage quality + coverage_analysis = self._perform_spatial_coverage_analysis(sorted_signals, mesh_nodes) + + # VENN DIAGRAM ANALYSIS - RESTORED! + venn_data = self._generate_venn_analysis(mesh_nodes) + + # Convert sets to lists for JSON serialization + for node in mesh_nodes.values(): + if isinstance(node['bands'], set): + node['bands'] = list(node['bands']) + + result = { + 'type': 'mesh', + 'brand': brand or 'unknown', + 'mesh_type': mesh_type, + 'total_nodes': total_nodes, + 'total_radios': len(same_ssid_aps), + 'bands': sorted(list(all_bands)), + 'signal_range': signal_range, + 'mesh_nodes': mesh_nodes, + 'signal_distribution': sorted_signals, + 'coverage_analysis': coverage_analysis, + 'venn_analysis': venn_data # ADDED: Venn diagram data + } + + # Legacy fields for compatibility + result['topology_health'] = coverage_analysis['topology_classification'] + result['coverage_reason'] = coverage_analysis['summary'] + result['coverage_health'] = coverage_analysis['spatial_distribution'] + result['coverage_details'] = { + 'signal_range': signal_range, + 'strongest_node': max(signals), + 'weakest_node': min(signals), + 'total_nodes': total_nodes, + 'radios_per_node': len(same_ssid_aps) / total_nodes + } + + return result + + def _generate_venn_analysis(self, mesh_nodes: Dict) -> Dict: + """Generate Venn diagram analysis for mesh overlap visualization""" + try: + venn_calculator = MeshVennCalculator() + + # Prepare nodes data for Venn analysis + nodes_for_venn = [] + for i, (node_id, node_data) in enumerate(mesh_nodes.items()): + node_for_venn = { + 'id': i, + 'label': f"Node {node_id[-8:]}", + 'signal': node_data['strongest_signal'], + 'bssid': node_id, + 'radios': len(node_data['radios']), + 'bands': list(node_data['bands']) if isinstance(node_data['bands'], set) else node_data['bands'] + } + nodes_for_venn.append(node_for_venn) + + # Generate Venn diagram data + venn_data = venn_calculator.generate_venn_data(nodes_for_venn) + + # Get overlap quality assessment + quality_assessment = venn_calculator.get_overlap_quality_assessment(venn_data) + + return { + 'venn_diagram': venn_data, + 'overlap_quality': quality_assessment, + 'total_nodes': len(nodes_for_venn), + 'overlap_count': venn_data.get('overlap_count', 0), + 'coverage_efficiency': quality_assessment.get('score', 0) + } + + except Exception as e: + # Fallback if Venn calculator fails + return { + 'venn_diagram': {'nodes': [], 'overlaps': [], 'total_coverage': 0}, + 'overlap_quality': {'quality': 'unavailable', 'score': 0, 'description': f'Venn analysis failed: {str(e)}'}, + 'total_nodes': len(mesh_nodes), + 'overlap_count': 0, + 'coverage_efficiency': 0 + } + + def _perform_spatial_coverage_analysis(self, sorted_signals: List[int], mesh_nodes: Dict) -> Dict: + """Perform sophisticated spatial coverage analysis based on signal distribution patterns""" + + # Calculate signal gradients and gaps + signal_gaps = [] + for i in range(len(sorted_signals) - 1): + gap = sorted_signals[i] - sorted_signals[i + 1] + signal_gaps.append(gap) + + max_gap = max(signal_gaps) if signal_gaps else 0 + avg_gap = sum(signal_gaps) / len(signal_gaps) if signal_gaps else 0 + + # Analyze coverage zones based on signal strength + zones = self._classify_coverage_zones(sorted_signals) + + # Detect coverage problems + coverage_issues = self._detect_coverage_issues(sorted_signals, signal_gaps, zones) + + # Overall topology assessment + topology_assessment = self._assess_mesh_topology(sorted_signals, signal_gaps, zones, coverage_issues) + + return { + 'sorted_signals': sorted_signals, + 'signal_gaps': signal_gaps, + 'max_signal_gap': max_gap, + 'avg_signal_gap': avg_gap, + 'coverage_zones': zones, + 'coverage_issues': coverage_issues, + 'topology_classification': topology_assessment['classification'], + 'summary': topology_assessment['summary'], + 'spatial_distribution': topology_assessment['distribution_analysis'], + 'recommendations': topology_assessment['recommendations'], + 'coverage_quality_score': topology_assessment['quality_score'] + } + + def _classify_coverage_zones(self, sorted_signals: List[int]) -> Dict: + """Classify coverage into spatial zones based on signal strength""" + zones = { + 'primary': [], # > -50dBm (excellent, close range) + 'secondary': [], # -50 to -65dBm (good, medium range) + 'tertiary': [], # -65 to -80dBm (fair, extended range) + 'fringe': [] # < -80dBm (poor, maximum range) + } + + for signal in sorted_signals: + if signal > -50: + zones['primary'].append(signal) + elif signal > -65: + zones['secondary'].append(signal) + elif signal > -80: + zones['tertiary'].append(signal) + else: + zones['fringe'].append(signal) + + return zones + + def _detect_coverage_issues(self, sorted_signals: List[int], signal_gaps: List[int], zones: Dict) -> List[Dict]: + """Detect specific coverage and distribution issues""" + issues = [] + + # Large signal gap detection (potential dead zones) + for i, gap in enumerate(signal_gaps): + if gap > 25: + issues.append({ + 'type': 'large_coverage_gap', + 'severity': 'high' if gap > 35 else 'medium', + 'details': f"{gap}dB gap between node {i+1} ({sorted_signals[i]}dBm) and node {i+2} ({sorted_signals[i+1]}dBm)", + 'impact': 'Potential dead zone or weak coverage area', + 'location': f"Between {self._signal_to_distance_estimate(sorted_signals[i])} and {self._signal_to_distance_estimate(sorted_signals[i+1])}" + }) + + # Coverage zone analysis + if not zones['secondary'] and zones['primary'] and zones['tertiary']: + issues.append({ + 'type': 'missing_intermediate_coverage', + 'severity': 'medium', + 'details': 'No medium-range coverage detected', + 'impact': 'May have coverage gaps between close and distant areas', + 'location': 'Medium-range areas (adjacent rooms/floors)' + }) + + # Clustering detection + if len(zones['primary']) > len(sorted_signals) * 0.6: + issues.append({ + 'type': 'node_clustering', + 'severity': 'low', + 'details': f"{len(zones['primary'])} of {len(sorted_signals)} nodes in primary zone", + 'impact': 'Possible over-concentration of nodes in small area', + 'location': 'Primary coverage area' + }) + + # Extended range without intermediate coverage + if zones['fringe'] and not zones['tertiary']: + issues.append({ + 'type': 'isolated_distant_node', + 'severity': 'medium', + 'details': f"Distant node at {min(sorted_signals)}dBm without intermediate coverage", + 'impact': 'Isolated coverage with potential gap to main mesh', + 'location': f"~{self._signal_to_distance_estimate(min(sorted_signals))}" + }) + + return issues + + def _assess_mesh_topology(self, sorted_signals: List[int], signal_gaps: List[int], zones: Dict, issues: List[Dict]) -> Dict: + """Comprehensive mesh topology assessment""" + + # Calculate quality score (0-100) + quality_score = 100 + + # Penalize for coverage issues + for issue in issues: + if issue['severity'] == 'high': + quality_score -= 25 + elif issue['severity'] == 'medium': + quality_score -= 15 + elif issue['severity'] == 'low': + quality_score -= 5 + + # Reward good signal distribution + if len(zones['secondary']) > 0: + quality_score += 10 # Good intermediate coverage + + if max(signal_gaps) < 20: + quality_score += 15 # Smooth signal transitions + + # Node count assessment + node_count = len(sorted_signals) + if node_count >= 4: + base_rating = "excellent_nodes" + node_assessment = f"{node_count} nodes detected - excellent for comprehensive coverage" + elif node_count == 3: + base_rating = "good_nodes" + node_assessment = f"{node_count} nodes detected - good for most home sizes" + elif node_count == 2: + base_rating = "basic_nodes" + node_assessment = f"{node_count} nodes detected - basic mesh configuration" + else: + base_rating = "single_node" + node_assessment = f"{node_count} node detected - not a true mesh" + + # Distribution analysis + max_gap = max(signal_gaps) if signal_gaps else 0 + + if max_gap > 30: + distribution = "poor_distribution" + dist_analysis = f"Large signal gaps detected (max {max_gap}dB) - potential coverage holes" + elif max_gap > 20: + distribution = "uneven_distribution" + dist_analysis = f"Moderate signal gaps (max {max_gap}dB) - some coverage irregularities" + elif max_gap > 10: + distribution = "good_distribution" + dist_analysis = f"Well-spaced nodes (max gap {max_gap}dB) - good coverage continuity" + else: + distribution = "excellent_distribution" + dist_analysis = f"Smooth signal transitions (max gap {max_gap}dB) - excellent spatial distribution" + + # Overall classification + high_severity_issues = [i for i in issues if i['severity'] == 'high'] + medium_severity_issues = [i for i in issues if i['severity'] == 'medium'] + + if high_severity_issues: + classification = "topology_issues" + summary = f"{node_assessment} but significant coverage gaps detected" + elif medium_severity_issues and node_count < 3: + classification = "basic_topology" + summary = f"{node_assessment} with some coverage limitations" + elif medium_severity_issues: + classification = "good_topology" + summary = f"{node_assessment} with minor coverage irregularities" + elif node_count >= 4 and quality_score > 85: + classification = "excellent_topology" + summary = f"{node_assessment} with excellent spatial distribution" + elif node_count >= 3 and quality_score > 75: + classification = "good_topology" + summary = f"{node_assessment} with good spatial coverage" + else: + classification = "basic_topology" + summary = f"{node_assessment} - adequate but could be optimized" + + # Recommendations + recommendations = [] + for issue in issues: + if issue['type'] == 'large_coverage_gap': + recommendations.append(f"Consider adding a node in {issue['location']} to eliminate coverage gap") + elif issue['type'] == 'missing_intermediate_coverage': + recommendations.append("Add intermediate nodes for smoother coverage transitions") + elif issue['type'] == 'node_clustering': + recommendations.append("Consider relocating some nodes for better spatial distribution") + elif issue['type'] == 'isolated_distant_node': + recommendations.append("Add intermediate nodes to bridge coverage to distant areas") + + if not recommendations and quality_score > 90: + recommendations.append("Excellent mesh topology - no improvements needed") + elif not recommendations: + recommendations.append("Good mesh topology - minor optimizations possible") + + return { + 'classification': classification, + 'summary': summary, + 'distribution_analysis': dist_analysis, + 'quality_score': max(0, min(100, quality_score)), + 'recommendations': recommendations, + 'node_assessment': node_assessment, + 'distribution_quality': distribution + } + + def _signal_to_distance_estimate(self, signal_dbm: int) -> str: + """Estimate approximate distance/location based on signal strength""" + if signal_dbm > -40: + return "very close (same room)" + elif signal_dbm > -50: + return "close (adjacent room)" + elif signal_dbm > -65: + return "medium range (different floor/far room)" + elif signal_dbm > -80: + return "extended range (distant area)" + else: + return "maximum range (basement/garage/far areas)" + + def _analyze_multiple_aps(self, same_ssid_aps: List[APScan]) -> Dict: + """Analyze multiple standalone APs with same SSID""" + signals = [ap.signal for ap in same_ssid_aps] + strongest_signal = max(signals) + + if strongest_signal > -50: + quality = "excellent" + quality_reason = f"Strong signals available ({strongest_signal}dBm) from multiple APs" + elif strongest_signal > -60: + quality = "good" + quality_reason = f"Good signal options ({strongest_signal}dBm) from {len(same_ssid_aps)} APs" + elif strongest_signal > -75: + quality = "fair" + quality_reason = f"Moderate signals ({strongest_signal}dBm) - consider moving closer to APs" + else: + quality = "poor" + quality_reason = f"Weak signals from all APs ({strongest_signal}dBm) - poor coverage area" + + # Convert APScan objects to dictionaries for JSON serialization + ap_list = [ap.to_dict() for ap in same_ssid_aps] + + return { + 'type': 'multiple_aps', + 'nodes': len(same_ssid_aps), + 'signal_quality': quality, + 'signal_reason': quality_reason, + 'strongest_signal': strongest_signal, + 'ap_list': ap_list + } + +class ProblemDetector: + """Detect WiFi connection problems from logs and events""" + + def __init__(self, history_tracker): + self.history = history_tracker + + def analyze_connection_patterns(self, window_hours: int = 24) -> Dict: + """Analyze connection patterns for problems""" + events = self.history.get_recent_events(window_hours) + + patterns = { + 'roaming_loops': [], + 'auth_failure_clusters': [], + 'rapid_disconnects': [], + 'time_based_issues': {}, + 'bssid_specific_problems': {} + } + + self._detect_roaming_loops(events, patterns) + self._detect_auth_clusters(events, patterns) + self._detect_rapid_cycles(events, patterns) + self._analyze_time_patterns(events, patterns) + self._analyze_bssid_problems(events, patterns) + + return patterns + + def _detect_roaming_loops(self, events: List[ConnectionEvent], patterns: Dict): + """Detect roaming loops between BSSIDs""" + connects = [e for e in events if e.event_type == 'connect'] + + for i in range(len(connects) - 3): + bssids = [connects[j].bssid for j in range(i, i + 4)] + if (bssids[0] == bssids[2] and bssids[1] == bssids[3] and bssids[0] != bssids[1]): + time_span = connects[i + 3].timestamp - connects[i].timestamp + if time_span < 300: # 5 minutes + patterns['roaming_loops'].append({ + 'bssids': [bssids[0], bssids[1]], + 'time_span': time_span, + 'start_time': connects[i].timestamp + }) + + def _detect_auth_clusters(self, events: List[ConnectionEvent], patterns: Dict): + """Detect clusters of authentication failures""" + auth_failures = [e for e in events if e.event_type == 'auth_timeout'] + + for bssid in set(e.bssid for e in auth_failures): + bssid_failures = [e for e in auth_failures if e.bssid == bssid] + + if len(bssid_failures) >= 3: + timestamps = sorted([e.timestamp for e in bssid_failures]) + clusters = [] + current_cluster = [timestamps[0]] + + for i in range(1, len(timestamps)): + if timestamps[i] - timestamps[i-1] < 300: # Within 5 minutes + current_cluster.append(timestamps[i]) + else: + if len(current_cluster) >= 3: + clusters.append(current_cluster) + current_cluster = [timestamps[i]] + + if len(current_cluster) >= 3: + clusters.append(current_cluster) + + for cluster in clusters: + patterns['auth_failure_clusters'].append({ + 'bssid': bssid, + 'failure_count': len(cluster), + 'time_span': cluster[-1] - cluster[0], + 'start_time': cluster[0] + }) + + def _detect_rapid_cycles(self, events: List[ConnectionEvent], patterns: Dict): + """Detect rapid disconnect/reconnect cycles""" + for i in range(len(events) - 1): + if (events[i].event_type == 'disconnect' and + events[i + 1].event_type == 'connect' and + events[i + 1].timestamp - events[i].timestamp < 60): + + patterns['rapid_disconnects'].append({ + 'bssid': events[i].bssid, + 'cycle_duration': events[i + 1].timestamp - events[i].timestamp + }) + + def _analyze_time_patterns(self, events: List[ConnectionEvent], patterns: Dict): + """Analyze time-based problem patterns""" + hourly_problems = defaultdict(list) + + for event in events: + if event.event_type in ['auth_timeout', 'disconnect']: + hour = datetime.fromtimestamp(event.timestamp).hour + hourly_problems[hour].append(event) + + for hour, hour_events in hourly_problems.items(): + if len(hour_events) >= 5: + patterns['time_based_issues'][hour] = { + 'problem_count': len(hour_events), + 'problem_types': list(set(e.event_type for e in hour_events)), + 'affected_bssids': list(set(e.bssid for e in hour_events)) + } + + def _analyze_bssid_problems(self, events: List[ConnectionEvent], patterns: Dict): + """Analyze per-BSSID specific problems""" + bssid_events = defaultdict(list) + + for event in events: + bssid_events[event.bssid].append(event) + + for bssid, bssid_event_list in bssid_events.items(): + problem_events = [e for e in bssid_event_list + if e.event_type in ['auth_timeout', 'disconnect']] + + if len(problem_events) >= 3: + patterns['bssid_specific_problems'][bssid] = { + 'total_problems': len(problem_events), + 'auth_failures': len([e for e in problem_events if e.event_type == 'auth_timeout']), + 'disconnects': len([e for e in problem_events if e.event_type == 'disconnect']), + 'problem_rate': len(problem_events) / len(bssid_event_list) if bssid_event_list else 0 + } + +class NetworkAnalyzer: + def __init__(self, interface: str): + self.interface = interface + self.signal_history = defaultdict(lambda: deque(maxlen=10)) + + # Initialize logging first + data_dir = self._get_data_dir() + self.log_manager = LogManager(Path(data_dir)) + + # Initialize components with logging + self.history_tracker = HistoryTracker(data_dir, self.log_manager) + self.mesh_intelligence = MeshIntelligence() + self.problem_detector = ProblemDetector(self.history_tracker) + self.connection_history = deque(maxlen=20) + # Initialize optional modules - Matt is testing some new functionality - the new detector classes + self.roaming_detector = None + self.power_detective = None + + if ROAMING_DETECTOR_AVAILABLE: + self.roaming_detector = MeshRoamingDetector(self.interface) + print("✅ Roaming detector module loaded") + + if POWER_DETECTIVE_AVAILABLE: + self.power_detective = MeshPowerDetective(self.interface) + print("✅ Power detective module loaded") + + # Log session start + self.log_manager.log_analysis_start(interface) + + # Start background monitoring + self._monitoring = True + self._monitor_thread = threading.Thread(target=self._background_monitor, daemon=True) + self._monitor_thread.start() + + def _format_power_data_for_html(self, power_issues): + """Format power issues data for HTML reporter""" + if not power_issues: + return {'issues_found': False} + + # Count issues by severity + severity_counts = {'high': 0, 'medium': 0, 'low': 0, 'info': 0} + total_issues = 0 + + for category_issues in power_issues.values(): + for issue in category_issues: + severity = issue.get('severity', 'low') + if severity in severity_counts: + severity_counts[severity] += 1 + total_issues += 1 + + return { + 'issues_found': total_issues > 0, + 'severity_counts': severity_counts, + 'total_issues': total_issues + } + + def _get_data_dir(self) -> str: + """Get data directory path""" + if os.geteuid() == 0 and 'SUDO_USER' in os.environ: + sudo_user = os.environ['SUDO_USER'] + import pwd + real_user_home = pwd.getpwnam(sudo_user).pw_dir + return os.path.join(real_user_home, ".mesh_analyzer") + else: + home = os.path.expanduser("~") + return os.path.join(home, ".mesh_analyzer") + + def _background_monitor(self): + """Background thread to monitor connection events""" + last_connection = None + connection_start = None + + while self._monitoring: + try: + current = self.get_current_connection() + + if current and (not last_connection or current['bssid'] != last_connection['bssid']): + # New connection + if last_connection and connection_start: + duration = time.time() - connection_start + self.history_tracker.record_event(ConnectionEvent( + timestamp=time.time(), + bssid=last_connection['bssid'], + event_type='disconnect', + signal=last_connection['signal'], + duration=duration + )) + + if current: + self.history_tracker.record_event(ConnectionEvent( + timestamp=time.time(), + bssid=current['bssid'], + event_type='connect', + signal=current['signal'] + )) + connection_start = time.time() + last_connection = current + + elif not current and last_connection: + # Disconnected + if connection_start: + duration = time.time() - connection_start + self.history_tracker.record_event(ConnectionEvent( + timestamp=time.time(), + bssid=last_connection['bssid'], + event_type='disconnect', + signal=last_connection['signal'], + duration=duration + )) + last_connection = None + connection_start = None + + time.sleep(10) # Check every 10 seconds + + except Exception: + time.sleep(30) + + def run_cmd(self, cmd: str, timeout: int = 8) -> str: + """Execute command with timeout and logging""" + try: + start_time = time.time() + result = subprocess.run(cmd, shell=True, text=True, + capture_output=True, timeout=timeout) + duration = time.time() - start_time + + output = result.stdout.strip() + if self.log_manager: + self.log_manager.log_command_execution(cmd, output, duration) + + return output + except Exception as e: + if self.log_manager: + self.log_manager.log_error(e, f"run_cmd: {cmd}") + return "" + + def get_current_connection(self) -> Optional[Dict]: + """Get current connection details""" + link_output = self.run_cmd(f"iw dev {self.interface} link") + if "Connected to" not in link_output: + return None + + bssid_match = re.search(r"Connected to ([0-9A-Fa-f:]{17})", link_output) + ssid_match = re.search(r"SSID:\s*(.*)", link_output) + freq_match = re.search(r"freq:\s*(\d+)", link_output) + signal_match = re.search(r"signal:\s*(-?\d+)", link_output) + + if not all([bssid_match, ssid_match, freq_match]): + return None + + # Clean BSSID + bssid_raw = bssid_match.group(1) + bssid_clean = bssid_raw.split('(')[0].strip().upper() + + if len(bssid_clean) != 17 or bssid_clean.count(':') != 5: + return None + + return { + 'ssid': ssid_match.group(1).strip(), + 'bssid': bssid_clean, + 'freq': int(freq_match.group(1)), + 'signal': int(signal_match.group(1)) if signal_match else -100 + } + + def comprehensive_scan(self) -> List[APScan]: + """Perform detailed network scan with logging""" + scan_start_time = time.time() + aps = {} + current_time = time.time() + + # Primary scan with iw + scan_output = self.run_cmd(f"iw dev {self.interface} scan flush", timeout=15) + if not scan_output: + scan_output = self.run_cmd(f"iw dev {self.interface} scan", timeout=12) + + current_ap = {} + for line in scan_output.split('\n'): + line = line.strip() + + if line.startswith('BSS '): + if current_ap and 'bssid' in current_ap: + ap = self._parse_ap_data(current_ap, current_time) + if ap and ap.ssid != '': # Filter hidden SSIDs + aps[ap.bssid] = ap + + # Extract BSSID + bss_part = line[4:].strip() + if len(bss_part) >= 17: + bssid = bss_part[:17].upper() + if bssid.count(':') == 5: + current_ap = {'bssid': bssid, 'capabilities': set()} + else: + current_ap = {} + else: + current_ap = {} + + elif current_ap: + if 'SSID:' in line: + current_ap['ssid'] = line.split('SSID: ', 1)[1] if ': ' in line else '' + elif 'freq:' in line: + freq_match = re.search(r'freq: (\d+)', line) + if freq_match: + current_ap['freq'] = int(freq_match.group(1)) + elif 'signal:' in line: + signal_match = re.search(r'signal: (-?\d+\.\d+)', line) + if signal_match: + current_ap['signal'] = int(float(signal_match.group(1))) + + # Handle last AP + if current_ap and 'bssid' in current_ap: + ap = self._parse_ap_data(current_ap, current_time) + if ap and ap.ssid != '': + aps[ap.bssid] = ap + + scan_duration = time.time() - scan_start_time + ap_list = list(aps.values()) + + # Log scan results + if self.log_manager: + self.log_manager.log_network_scan(len(ap_list), scan_duration) + + return ap_list + + def _parse_ap_data(self, ap_data: dict, timestamp: float) -> Optional[APScan]: + """Create APScan from parsed data""" + try: + return APScan( + ssid=ap_data.get('ssid', ''), + bssid=ap_data['bssid'], + freq=ap_data.get('freq', 0), + signal=ap_data.get('signal', -100), + capabilities=ap_data.get('capabilities', set()), + last_seen=timestamp + ) + except KeyError: + return None + + def _analyze_available_alternatives(self, current_conn: Dict) -> List[Dict]: + """Analyze available BSSID alternatives with smarter band-aware scoring""" + same_ssid_aps = [ap for ap in getattr(self, '_current_aps', []) if ap.ssid == current_conn['ssid']] + + alternatives = [] + current_bssid = current_conn['bssid'].upper() + current_band = self._get_band_from_freq(current_conn['freq']) + current_signal = current_conn['signal'] + + for ap in same_ssid_aps: + if ap.bssid.upper() == current_bssid: + continue + + # Get historical performance + history = self.history_tracker.get_bssid_performance(ap.bssid) + alt_band = self._get_band_from_freq(ap.freq) + + # Score this alternative with smarter logic + score = 100 + recommendation_reasons = [] + + # Historical performance scoring + if history and history.stability_score > 0: + score += min(history.stability_score * 0.3, 30) + recommendation_reasons.append(f"Stability: {history.stability_score:.0f}%") + else: + recommendation_reasons.append("No historical data") + + # Smart signal evaluation - consider both strength and band capabilities + signal_diff = ap.signal - current_signal + + # Only recommend if there's a compelling reason + compelling_reason = False + + # Case 1: Significantly stronger signal (>15dB) on any band + if signal_diff > 15: + score += 25 + compelling_reason = True + recommendation_reasons.append(f"Major signal boost (+{signal_diff}dB)") + + # Case 2: Current signal is weak (<-70dBm) and alternative is stronger + elif current_signal < -70 and signal_diff > 5: + score += 20 + compelling_reason = True + recommendation_reasons.append(f"Escape weak signal zone (+{signal_diff}dB)") + + # Case 3: Moving to 5GHz from 2.4GHz with good signal + elif current_band == '2.4GHz' and alt_band == '5GHz' and ap.signal > -65: + score += 15 + compelling_reason = True + recommendation_reasons.append(f"5GHz upgrade opportunity ({ap.signal}dBm)") + + # Case 4: Current 6GHz signal is marginal (<-60dBm) and 5GHz/2.4GHz is much stronger + elif current_band == '6GHz' and current_signal < -60 and signal_diff > 10: + score += 10 + compelling_reason = True + recommendation_reasons.append(f"6GHz signal marginal, better alternative (+{signal_diff}dB)") + + # Otherwise, be conservative about recommendations + else: + # Penalize downgrades from high-performance bands with good signals + if current_band == '6GHz' and current_signal > -60 and alt_band in ['2.4GHz', '5GHz']: + score -= 30 + recommendation_reasons.append(f"Potential speed downgrade from {current_band}") + elif current_band == '5GHz' and current_signal > -65 and alt_band == '2.4GHz': + score -= 20 + recommendation_reasons.append(f"Potential speed downgrade from {current_band}") + else: + recommendation_reasons.append(f"Minimal benefit (+{signal_diff}dB)") + + # Band-specific bonuses only when it makes sense + if alt_band == '5GHz' and (current_band == '2.4GHz' or (current_band == '6GHz' and current_signal < -65)): + score += 5 + recommendation_reasons.append("Good speed/range balance") + elif alt_band == '6GHz' and current_band != '6GHz' and ap.signal > -55: + score += 10 + recommendation_reasons.append("Maximum speed potential") + elif alt_band == '2.4GHz' and current_signal < -75: + score += 5 + recommendation_reasons.append("Better range/penetration") + + # Signal quality assessment + if ap.signal > -50: + recommendation_reasons.append(f"Excellent signal ({ap.signal}dBm)") + elif ap.signal > -60: + recommendation_reasons.append(f"Good signal ({ap.signal}dBm)") + elif ap.signal > -70: + recommendation_reasons.append(f"Fair signal ({ap.signal}dBm)") + else: + recommendation_reasons.append(f"Weak signal ({ap.signal}dBm)") + score -= 15 + + # Determine overall recommendation based on compelling reasons + if compelling_reason and score >= 120: + recommendation = "EXCELLENT" + elif compelling_reason and score >= 100: + recommendation = "GOOD" + elif score >= 90: + recommendation = "FAIR" + else: + recommendation = "POOR" + + alternatives.append({ + 'bssid': ap.bssid, + 'signal': ap.signal, + 'freq': ap.freq, + 'score': score, + 'recommendation': recommendation, + 'reasons': recommendation_reasons, + 'signal_diff': signal_diff, + 'stability_score': history.stability_score if history else None, + 'compelling_reason': compelling_reason, + 'band': alt_band + }) + + # Sort by score (best first) + alternatives.sort(key=lambda x: x['score'], reverse=True) + return alternatives[:5] # Show top 5 + + def _get_band_from_freq(self, freq: int) -> str: + """Get band name from frequency""" + if 2400 <= freq <= 2500: + return '2.4GHz' + elif 5000 <= freq <= 5999: + return '5GHz' + elif 6000 <= freq <= 7125: + return '6GHz' + else: + return f'{freq}MHz' + + def generate_html_report(self): + """Generate interactive HTML report of mesh analysis with Venn overlap""" + try: + print("\n🌐 GENERATING HTML REPORT") + print("─" * 60) + + # Get current connection + current_conn = self.get_current_connection() + + # Use existing scan data or perform new scan + if not hasattr(self, '_current_aps') or not self._current_aps: + print("🔍 Scanning networks for HTML report...") + aps = self.comprehensive_scan() + self._current_aps = aps + else: + aps = self._current_aps + + # Prepare comprehensive analysis data + analysis_data = {} + + if current_conn: + same_ssid_aps = [ap for ap in aps if ap.ssid == current_conn['ssid']] + + # Mesh topology analysis + print("📊 Analyzing mesh topology...") + mesh_analysis = self.mesh_intelligence.analyze_mesh_topology(same_ssid_aps) + analysis_data['mesh_analysis'] = mesh_analysis + + # Alternative options analysis + if len(same_ssid_aps) > 1: + print("🔍 Evaluating alternatives...") + alternatives = self._analyze_available_alternatives(current_conn) + analysis_data['alternatives'] = alternatives + else: + analysis_data['alternatives'] = [] + + # Historical performance data + print("📈 Gathering historical data...") + current_history = self.history_tracker.get_bssid_performance(current_conn['bssid']) + if current_history: + analysis_data['historical_data'] = { + 'stability_score': current_history.stability_score, + 'total_connections': current_history.total_connections, + 'success_rate': (current_history.successful_connections / max(current_history.total_connections, 1) * 100), + 'avg_signal': current_history.avg_signal, + 'auth_failures': current_history.auth_failures, + 'disconnects': current_history.disconnects + } + else: + analysis_data['historical_data'] = {} + + # Problem pattern detection + print("🚨 Detecting problems...") + problems = self.problem_detector.analyze_connection_patterns(24) + analysis_data['problems'] = problems + else: + # No connection - provide empty data + analysis_data = { + 'mesh_analysis': {'type': 'no_connection'}, + 'alternatives': [], + 'historical_data': {}, + 'problems': {} + } + + # Include roaming and power data if available + analysis_data['roaming_data'] = getattr(self, 'roaming_data', {}) + analysis_data['power_data'] = getattr(self, 'power_data', {}) + + # Generate the HTML report + print("📝 Generating HTML visualization with mesh overlap analysis...") + + # Check if we have the updated HTML reporter + if UPDATED_HTML_REPORTER_AVAILABLE: + reporter = MeshHTMLReporter() + report_path = reporter.generate_report(analysis_data, current_conn) + else: + # Fallback to basic HTML generation + report_path = self._generate_basic_html_report(analysis_data, current_conn) + + if report_path: + print(f"✅ HTML Report Generated Successfully!") + print(f" 📁 Location: {report_path}") + print(f" 🌐 Open in browser: file://{report_path}") + print(f" 📊 Report includes: mesh topology, signal analysis, recommendations, historical data") + + # Log the report generation + if self.log_manager: + self.log_manager.analysis_logger.info(f"HTML report generated: {report_path}") + else: + print("❌ Failed to generate HTML report") + + return report_path + + except Exception as e: + print(f"❌ Error generating HTML report: {e}") + if hasattr(self, 'log_manager'): + self.log_manager.log_error(e, "generate_html_report") + import traceback + traceback.print_exc() + return None + + def _generate_basic_html_report(self, analysis_data, current_conn): + """Fallback basic HTML report generator""" + try: + from datetime import datetime + from pathlib import Path + + # Create reports directory - FIXED: Use same logic as data_dir for consistency + data_dir = Path(self._get_data_dir()) + reports_dir = data_dir / "reports" + reports_dir.mkdir(parents=True, exist_ok=True) + + # Fix permissions if running as sudo + if os.geteuid() == 0 and 'SUDO_USER' in os.environ: + sudo_user = os.environ['SUDO_USER'] + import pwd + user_info = pwd.getpwnam(sudo_user) + os.chown(reports_dir, user_info.pw_uid, user_info.pw_gid) + + # Generate filename + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + filename = f"mesh_analysis_{timestamp}.html" + report_path = reports_dir / filename + + # Basic HTML content + html_content = f""" + + + + + WiFi Mesh Analysis Report + + + +
+
+

WiFi Mesh Network Analysis

+

Generated: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}

+
+ +
+

Current Connection

+ {self._format_connection_html(current_conn)} +
+ +
+

Network Analysis

+ {self._format_mesh_analysis_html(analysis_data.get('mesh_analysis', {}))} +
+ +
+

Analysis Data

+
{json.dumps(analysis_data, indent=2, default=str)}
+
+
+ +""" + + # Write file + with open(report_path, 'w', encoding='utf-8') as f: + f.write(html_content) + + return str(report_path) + + except Exception as e: + print(f"❌ Error in basic HTML generation: {e}") + return None + + def _format_connection_html(self, current_conn): + """Format current connection for basic HTML""" + if not current_conn: + return "

Not connected to any network

" + + return f""" +

SSID: {current_conn.get('ssid', 'Unknown')}

+

BSSID: {current_conn.get('bssid', 'Unknown')}

+

Signal: {current_conn.get('signal', -100)} dBm

+

Frequency: {current_conn.get('freq', 0)} MHz

+ """ + + def _format_mesh_analysis_html(self, mesh_analysis): + """Format mesh analysis for basic HTML""" + if not mesh_analysis: + return "

No mesh analysis data available

" + + network_type = mesh_analysis.get('type', 'unknown') + + if network_type == 'mesh': + return f""" +

Network Type: Mesh ({mesh_analysis.get('total_nodes', 0)} nodes)

+

Brand: {mesh_analysis.get('brand', 'Unknown')}

+

Mesh Type: {mesh_analysis.get('mesh_type', 'Unknown')}

+

Total Radios: {mesh_analysis.get('total_radios', 0)}

+

Bands: {', '.join(mesh_analysis.get('bands', []))}

+

Topology Health: {mesh_analysis.get('topology_health', 'Unknown')}

+ """ + else: + return f""" +

Network Type: {network_type.replace('_', ' ').title()}

+

Signal Quality: {mesh_analysis.get('signal_quality', 'Unknown')}

+ """ + + def run_analysis(self): + """Run complete network analysis WITHOUT auto-generating HTML""" + try: + print("🧠 WiFi Mesh Network Analyzer") + print("=" * 60) + print("🔍 Analysis: Signal Intelligence • Mesh Topology • Historical Tracking • Pattern Recognition • Venn Overlap") + print("=" * 60) + + # Get current connection + current_conn = self.get_current_connection() + print(f"📡 Interface: {self.interface}") + + if current_conn: + print(f"🔗 Connected: {current_conn['ssid']} | {current_conn['bssid']} | " + + f"{current_conn['freq']} MHz | {current_conn['signal']} dBm") + else: + print("❌ Not connected to any network") + print("") + + # Scan networks + print("📊 NETWORK SCANNING") + print("─" * 60) + print("🔍 Scanning networks with historical analysis...") + aps = self.comprehensive_scan() + self._current_aps = aps + print(f"📡 Found {len(aps)} access points") + + # Enhanced mesh analysis + if current_conn: + same_ssid_aps = [ap for ap in aps if ap.ssid == current_conn['ssid']] + if len(same_ssid_aps) > 1: + print("\n📊 MESH INTELLIGENCE") + print("─" * 60) + mesh_analysis = self.mesh_intelligence.analyze_mesh_topology(same_ssid_aps) + + # Log mesh analysis + if self.log_manager: + self.log_manager.log_mesh_analysis(mesh_analysis) + + self._display_mesh_analysis(mesh_analysis, current_conn) + else: + print("\n📊 SINGLE ACCESS POINT NETWORK") + print("─" * 60) + + # Historical performance analysis + print("\n📊 HISTORICAL PERFORMANCE") + print("─" * 60) + self._display_historical_analysis(current_conn) + + # Smart problem detection + print("\n📊 PROBLEM DETECTION") + print("─" * 60) + connection_patterns = self.problem_detector.analyze_connection_patterns(24) + + # Log problem detection + if self.log_manager: + self.log_manager.log_problems_detected(connection_patterns) + + self._display_pattern_analysis(connection_patterns) + + # Recommendations + if current_conn and len(same_ssid_aps) > 1: + print("\n📊 RECOMMENDATIONS") + print("─" * 60) + alternatives = self._analyze_available_alternatives(current_conn) + + # Log performance metrics and recommendations + if self.log_manager: + self.log_manager.log_performance_metrics(current_conn, alternatives) + + # Create recommendation data for logging + recommendations = self._create_recommendations_data(alternatives, current_conn) + self.log_manager.log_recommendations(recommendations) + + self._display_recommendations(alternatives, current_conn) + + except Exception as e: + print(f"❌ Analysis error: {e}") + if self.log_manager: + self.log_manager.log_error(e, "run_analysis") + + def _display_mesh_analysis(self, mesh_analysis: Dict, current_conn: Dict): + """Display mesh analysis results with clear BSSID connections""" + + if mesh_analysis['type'] == 'single_ap': + print(f"📡 Network Type: Single Access Point") + if 'signal_quality' in mesh_analysis: + quality = mesh_analysis['signal_quality'].replace('_', ' ').title() + reason = mesh_analysis['signal_reason'] + + if mesh_analysis['signal_quality'] == 'excellent': + emoji = "🟢" + elif mesh_analysis['signal_quality'] == 'good': + emoji = "🟡" + elif mesh_analysis['signal_quality'] == 'fair': + emoji = "🟠" + else: + emoji = "🔴" + + print(f"📶 Signal Quality: {emoji} {quality}") + print(f" 📊 Analysis: {reason}") + + if mesh_analysis['signal_quality'] in ['fair', 'poor']: + print(f" 💡 Recommendations:") + if mesh_analysis['signal_quality'] == 'poor': + print(f" 1. Move closer to the access point") + print(f" 2. Check for physical obstructions") + print(f" 3. Consider relocating the AP to a more central location") + print(f" 4. Verify AP placement is elevated and away from interference") + else: + print(f" 1. Minor positioning adjustments may help") + print(f" 2. Check for interference sources nearby") + return + + elif mesh_analysis['type'] == 'multiple_aps': + print(f"📡 Network Type: Multiple Access Points (Same SSID)") + print(f"🏠 Configuration: {mesh_analysis['nodes']} standalone APs") + + quality = mesh_analysis['signal_quality'].replace('_', ' ').title() + reason = mesh_analysis['signal_reason'] + + if mesh_analysis['signal_quality'] == 'excellent': + emoji = "🟢" + elif mesh_analysis['signal_quality'] == 'good': + emoji = "🟡" + elif mesh_analysis['signal_quality'] == 'fair': + emoji = "🟠" + else: + emoji = "🔴" + + print(f"📶 Coverage Quality: {emoji} {quality}") + print(f" 📊 Analysis: {reason}") + return + + # Mesh system analysis + print(f"🏷️ Brand: {mesh_analysis.get('brand', 'Unknown').replace('_', ' ').title()}") + print(f"🔧 Type: {mesh_analysis['mesh_type'].replace('_', '-').title()} Mesh") + print(f"🏠 Topology: {mesh_analysis['total_nodes']} nodes, {mesh_analysis['total_radios']} radios") + print(f" ℹ️ Note: Only shows nodes visible from your current location") + print(f" 📊 Why some nodes may be missing:") + print(f" • Distant nodes (basement, far rooms) may be too weak to detect") + print(f" • Nodes powered off or disconnected from mesh") + print(f" • Interference blocking weak signals from remote areas") + print(f" • Your device's WiFi antenna limitations") + + # Enhanced mesh topology analysis with spatial intelligence + topology_health = mesh_analysis['topology_health'].replace('_', ' ').title() + coverage_analysis = mesh_analysis.get('coverage_analysis', {}) + quality_score = coverage_analysis.get('coverage_quality_score', 0) + + if mesh_analysis['topology_health'] in ['excellent_topology', 'good_topology']: + emoji = "🟢" + elif mesh_analysis['topology_health'] == 'basic_topology': + emoji = "🟡" + else: + emoji = "🟠" + + print(f"📶 Mesh Topology: {emoji} {topology_health} (Quality Score: {quality_score:.0f}/100)") + print(f" 📊 Analysis: {mesh_analysis.get('coverage_reason', 'Analysis pending')}") + + # Show spatial coverage with current connection context + zones = coverage_analysis.get('coverage_zones', {}) + mesh_nodes = mesh_analysis.get('mesh_nodes', {}) + + if zones: + print(f"\n🗺️ SPATIAL COVERAGE ANALYSIS:") + print(f" 📍 Coverage Zones & Your Connection:") + + # Find which node/radio the user is connected to + current_node_info = None + current_radio_info = None + + for node_id, node_data in mesh_nodes.items(): + for radio in node_data.get('radios', []): + if radio['bssid'] == current_conn['bssid']: + current_node_info = node_data + current_radio_info = radio + break + if current_node_info: + break + + # Display zones with current connection context + if zones.get('primary'): + signals = zones['primary'] + print(f" 🟢 Primary Zone: {len(signals)} nodes ({min(signals)} to {max(signals)}dBm)") + print(f" └─ Excellent coverage area (same room/very close)") + + if current_radio_info and current_radio_info['signal'] in signals: + print(f" 🔗 YOU ARE HERE: Connected to {current_conn['bssid']} at {current_conn['signal']}dBm") + if len(signals) > 1: + other_signals = [s for s in signals if s != current_conn['signal']] + if other_signals: + print(f" 💡 {len(other_signals)} other excellent nodes available in this zone") + + if zones.get('secondary'): + signals = zones['secondary'] + print(f" 🟡 Secondary Zone: {len(signals)} nodes ({min(signals)} to {max(signals)}dBm)") + print(f" └─ Good coverage area (adjacent rooms/floors)") + + if current_radio_info and current_radio_info['signal'] in signals: + print(f" 🔗 YOU ARE HERE: Connected to {current_conn['bssid']} at {current_conn['signal']}dBm") + primary_available = len(zones.get('primary', [])) + if primary_available > 0: + print(f" 💡 Consider moving closer - {primary_available} stronger nodes available") + + if zones.get('tertiary'): + signals = zones['tertiary'] + print(f" 🟠 Tertiary Zone: {len(signals)} nodes ({min(signals)} to {max(signals)}dBm)") + print(f" └─ Extended coverage area (distant rooms)") + + if current_radio_info and current_radio_info['signal'] in signals: + print(f" 🔗 YOU ARE HERE: Connected to {current_conn['bssid']} at {current_conn['signal']}dBm") + better_zones = len(zones.get('primary', [])) + len(zones.get('secondary', [])) + if better_zones > 0: + print(f" 💡 {better_zones} stronger nodes available - consider moving closer to mesh") + + if zones.get('fringe'): + signals = zones['fringe'] + print(f" 🔴 Fringe Zone: {len(signals)} nodes ({min(signals)} to {max(signals)}dBm)") + print(f" └─ Maximum range coverage (basement/garage/far areas)") + + if current_radio_info and current_radio_info['signal'] in signals: + print(f" 🔗 YOU ARE HERE: Connected to {current_conn['bssid']} at {current_conn['signal']}dBm") + print(f" ⚠️ You're at maximum range - consider moving closer for better performance") + better_zones = len(zones.get('primary', [])) + len(zones.get('secondary', [])) + len(zones.get('tertiary', [])) + if better_zones > 0: + print(f" 💡 {better_zones} stronger nodes available") + + # If current connection not found in any zone, show fallback info + if not current_radio_info: + print(f" 🔗 Current Connection: {current_conn['bssid']} at {current_conn['signal']}dBm") + print(f" 📊 Note: Unable to match current BSSID to detected mesh nodes") + + print(f"📡 Bands: {', '.join(mesh_analysis['bands'])}") + + # VENN DIAGRAM ANALYSIS - RESTORED! + venn_analysis = mesh_analysis.get('venn_analysis', {}) + if venn_analysis and venn_analysis.get('venn_diagram'): + print(f"\n🔄 VENN OVERLAP ANALYSIS:") + overlap_quality = venn_analysis.get('overlap_quality', {}) + quality = overlap_quality.get('quality', 'unknown') + score = overlap_quality.get('score', 0) + + if quality == 'excellent': + quality_emoji = "🟢" + elif quality == 'good': + quality_emoji = "🟡" + elif quality == 'fair': + quality_emoji = "🟠" + else: + quality_emoji = "🔴" + + print(f" {quality_emoji} Coverage Overlap Quality: {quality.title()} (Score: {score}/100)") + print(f" 📊 {overlap_quality.get('description', 'No description available')}") + + venn_data = venn_analysis['venn_diagram'] + overlap_count = venn_data.get('overlap_count', 0) + if overlap_count > 0: + print(f" 🔗 Detected {overlap_count} node overlaps") + overlaps = venn_data.get('overlaps', []) + for overlap in overlaps[:3]: # Show top 3 overlaps + print(f" • {overlap.get('node1_label', 'Node')} ↔ {overlap.get('node2_label', 'Node')}: {overlap.get('overlap_percentage', 0):.1f}% overlap") + else: + print(f" ⚠️ No significant node overlaps detected - potential coverage gaps") + + def _display_historical_analysis(self, current_conn: Optional[Dict]): + """Display detailed historical performance analysis with context""" + if not current_conn: + print("📊 Connect to a network for historical analysis") + return + + # Current BSSID history with detailed breakdown + current_history = self.history_tracker.get_bssid_performance(current_conn['bssid']) + if current_history: + print(f"📈 Current BSSID Performance Analysis ({current_conn['bssid']}):") + + stability = current_history.stability_score + if stability >= 90: + stability_rating = "Excellent" + stability_emoji = "🟢" + elif stability >= 75: + stability_rating = "Good" + stability_emoji = "🟡" + elif stability >= 60: + stability_rating = "Fair" + stability_emoji = "🟠" + else: + stability_rating = "Poor" + stability_emoji = "🔴" + + print(f" {stability_emoji} Stability Score: {stability:.1f}/100 ({stability_rating})") + print(f" 🔄 Connection History: {current_history.total_connections} total attempts") + + success_rate = (current_history.successful_connections/max(current_history.total_connections,1)*100) + print(f" ✅ Success Rate: {success_rate:.1f}%") + + else: + print(f"📊 No historical data for current BSSID ({current_conn['bssid']})") + print(f" 📝 This appears to be a new connection") + + def _display_pattern_analysis(self, patterns: Dict): + """Display smart problem detection results""" + total_issues = (len(patterns['roaming_loops']) + + len(patterns['auth_failure_clusters']) + + len(patterns['rapid_disconnects'])) + + if total_issues == 0: + print("✅ No problematic patterns detected") + else: + print(f"🚨 {total_issues} problematic patterns detected:") + if patterns['roaming_loops']: + print(f" 🔄 Roaming Loops: {len(patterns['roaming_loops'])}") + if patterns['auth_failure_clusters']: + print(f" 🔐 Auth Failure Clusters: {len(patterns['auth_failure_clusters'])}") + if patterns['rapid_disconnects']: + print(f" ⚡ Rapid Reconnects: {len(patterns['rapid_disconnects'])}") + + def _display_recommendations(self, alternatives: List[Dict], current_conn: Dict): + """Display smart, realistic recommendations""" + if not alternatives: + print("📊 Current BSSID appears to be the best available option") + return + + best = alternatives[0] + should_recommend = ( + best.get('compelling_reason', False) and + best['score'] > 110 and + (best['signal_diff'] > 5 or current_conn['signal'] < -70) + ) + + if should_recommend: + print("💡 PERFORMANCE OPTIMIZATION OPPORTUNITY:") + print(f" 🎯 Recommended BSSID: {best['bssid']}") + print(f" 📈 Expected improvement: {best['signal_diff']:+d}dB signal strength") + print(f" 🏆 Quality rating: {best['recommendation']}") + else: + print("✅ CURRENT CONNECTION IS OPTIMAL") + print(f" 📊 Analysis: Your current connection is performing well") + + def _create_recommendations_data(self, alternatives: List[Dict], current_conn: Dict) -> Dict: + """Create structured recommendation data for logging""" + if not alternatives: + return {'action_recommended': False, 'reason': 'No beneficial alternatives found'} + + best = alternatives[0] + if best['score'] > 110 and best['signal_diff'] > 5: + return { + 'action_recommended': True, + 'action': 'Switch to stronger radio/node', + 'target_bssid': best['bssid'], + 'signal_improvement': best['signal_diff'], + 'priority': 'HIGH' if best['signal_diff'] > 15 else 'MODERATE', + 'score': best['score'] + } + else: + return {'action_recommended': False, 'reason': 'Current connection is optimal'} + + def create_log_archive(self) -> str: + """Create and return path to compressed log archive""" + try: + archive_path = self.log_manager.create_analysis_archive() + if archive_path: + print(f"\n📦 Log archive created: {archive_path}") + return archive_path + except Exception as e: + print(f"❌ Error creating log archive: {e}") + return "" + +def main(): + import argparse + + parser = argparse.ArgumentParser(description="WiFi Mesh Network Analyzer - Analysis and Recommendations with Venn Overlap") + parser.add_argument("--monitor", action="store_true", + help="Continuous monitoring mode") + parser.add_argument("--storage-info", action="store_true", + help="Show history storage information") + parser.add_argument("--scan-interval", type=int, default=60, + help="Scan interval in seconds for monitoring mode (default: 60)") + parser.add_argument("--reset-history", action="store_true", + help="Reset corrupted history files") + parser.add_argument("--create-archive", action="store_true", + help="Create compressed archive of logs after analysis") + parser.add_argument("--archive-only", action="store_true", + help="Create archive without running new analysis") + parser.add_argument("--html-report", action="store_true", + help="Generate interactive HTML report after analysis") + # Here we go, folks, Matt adding more cool functionality to test in this bad boy - 5 new command-line options. + parser.add_argument("--detect-dropouts", action="store_true", + help="Detect micro-dropouts and connection interruptions") + parser.add_argument("--roaming-test", action="store_true", + help="Run roaming quality test (walk around during test)") + parser.add_argument("--monitor-roaming", action="store_true", + help="Continuously monitor roaming events") + parser.add_argument("--check-power", action="store_true", + help="Check for WiFi power management issues") + args = parser.parse_args() + + # Find Wi-Fi interface + iface_cmd = ("nmcli -t --escape no -f DEVICE,TYPE device status " + "| awk -F: '$2==\"wifi\"{print $1;exit}'") + interface = subprocess.run(iface_cmd, shell=True, text=True, + capture_output=True).stdout.strip() + + if not interface: + print("❌ No Wi-Fi interface found") + return + + analyzer = NetworkAnalyzer(interface) + + try: + if args.archive_only: + # Just create archive without new analysis + print("📦 Creating log archive from existing data...") + archive_path = analyzer.create_log_archive() + if archive_path: + print(f"✅ Archive ready: {archive_path}") + else: + print("❌ Failed to create archive") + + if args.storage_info: + # Show storage information + storage_info = { + 'storage_path': str(analyzer.history_tracker.data_dir), + 'bssid_count': len(analyzer.history_tracker.bssid_history), + 'event_count': len(analyzer.history_tracker.connection_events), + 'storage_exists': analyzer.history_tracker.data_dir.exists(), + 'history_file_exists': analyzer.history_tracker.history_file.exists(), + 'events_file_exists': analyzer.history_tracker.events_file.exists() + } + + print("📁 Mesh Analyzer Storage Information") + print("=" * 50) + print(f"📂 Storage Location: {storage_info['storage_path']}") + print(f"📊 BSSID Records: {storage_info['bssid_count']}") + print(f"📈 Connection Events: {storage_info['event_count']}") + print(f"💾 Directory Exists: {'✅' if storage_info['storage_exists'] else '❌'}") + print(f"📄 History File: {'✅' if storage_info['history_file_exists'] else '❌'}") + print(f"📄 Events File: {'✅' if storage_info['events_file_exists'] else '❌'}") + + if args.reset_history: + # Reset corrupted history files + print("🔄 Resetting Mesh Analyzer History") + print("=" * 40) + + if analyzer.history_tracker.history_file.exists() or analyzer.history_tracker.events_file.exists(): + print("📦 Backing up existing files...") + analyzer.history_tracker._backup_and_reset_corrupted_files() + print("✅ History reset complete - fresh tracking will begin") + else: + print("📂 No existing history files found") + print("💡 History tracking will start automatically on next run") + + if args.monitor: + # Continuous monitoring mode + print("🔄 Continuous monitoring mode (Ctrl+C to stop)") + while True: + analyzer.run_analysis() + print(f"\n⏰ Next scan in {args.scan_interval} seconds...\n") + time.sleep(args.scan_interval) + + # Matt adding yet more oddly commented features - roaming issue detection, redux. + if args.detect_dropouts: + if analyzer.roaming_detector: + print("\n🔍 MICRO-DROPOUT DETECTION") + print("=" * 60) + analyzer.roaming_detector.detect_microdropouts(duration=30) + else: + print("❌ Roaming detector module not available") + print("💡 Make sure mesh_roaming_detector.py is in the same directory") + + if args.roaming_test: + if analyzer.roaming_detector: + print("\n🚶 ROAMING QUALITY TEST") + print("=" * 60) + roaming_data = analyzer.roaming_detector.measure_roaming_performance(walk_test=True) + analyzer.roaming_data = roaming_data + else: + print("❌ Roaming detector module not available") + print("💡 Make sure mesh_roaming_detector.py is in the same directory") + + if args.monitor_roaming: + if analyzer.roaming_detector: + print("\n📊 CONTINUOUS ROAMING MONITOR") + print("=" * 60) + analyzer.roaming_detector.continuous_quality_monitor() + else: + print("❌ Roaming detector module not available") + print("💡 Make sure mesh_roaming_detector.py is in the same directory") + + # Matt adding yet more poorly commented features - scan of all WiFi power management settings. + if args.check_power: + if analyzer.power_detective: + print("\n🔋 WIFI POWER MANAGEMENT CHECK") + print("=" * 60) + # Capture the power issues data + power_issues = analyzer.power_detective.check_all_power_issues() + + # Format and store for HTML reporter + analyzer.power_data = analyzer._format_power_data_for_html(power_issues) + + else: + print("❌ Power detective module not available") + print("💡 Make sure mesh_power_detective.py is in the same directory") + + if args.html_report: + # Generate HTML report after analysis - FIXED: No duplicate generation + analyzer.run_analysis() + print("\n" + "="*60) + analyzer.generate_html_report() + + elif not any([args.archive_only, args.storage_info, args.reset_history, args.monitor, + args.detect_dropouts, args.roaming_test, args.monitor_roaming, args.check_power]): + # Default: Single analysis run only if no other options specified + analyzer.run_analysis() + + # Create archive if requested + if args.create_archive: + print("\n" + "="*60) + archive_path = analyzer.create_log_archive() + if archive_path: + print(f"✅ Analysis complete with archived logs: {archive_path}") + else: + print("⚠️ Analysis complete but archive creation failed") + + except KeyboardInterrupt: + analyzer._monitoring = False + print("\n👋 Analysis stopped") + except Exception as e: + print(f"❌ Error: {e}") + import traceback + traceback.print_exc() + if hasattr(analyzer, 'log_manager'): + analyzer.log_manager.log_error(e, "main") + +if __name__ == "__main__": + main() diff --git a/MeshAnalyzer/files/mesh_html_reporter.py b/MeshAnalyzer/files/mesh_html_reporter.py new file mode 100644 index 0000000..d1eac24 --- /dev/null +++ b/MeshAnalyzer/files/mesh_html_reporter.py @@ -0,0 +1,1603 @@ +#!/usr/bin/env python3 +""" +Enhanced HTML Reporter for WiFi Mesh Analysis +- Beautiful, responsive HTML reports +- Mesh topology visualization with Venn overlap analysis +- Signal analysis and recommendations +- Roaming and power management integration +- Modern dark theme with glassmorphism design +""" + +import json +import os +from datetime import datetime +from pathlib import Path +from typing import Dict, List, Optional, Any + +class MeshHTMLReporter: + """Enhanced HTML reporter with roaming and power support""" + + def __init__(self): + # FIXED: Use same logic as main analyzer for consistent user directory + if os.geteuid() == 0 and 'SUDO_USER' in os.environ: + sudo_user = os.environ['SUDO_USER'] + import pwd + real_user_home = pwd.getpwnam(sudo_user).pw_dir + data_dir = os.path.join(real_user_home, ".mesh_analyzer") + else: + home = os.path.expanduser("~") + data_dir = os.path.join(home, ".mesh_analyzer") + + self.report_dir = Path(data_dir) / "reports" + self.report_dir.mkdir(parents=True, exist_ok=True) + + # Fix permissions if running as sudo + if os.geteuid() == 0 and 'SUDO_USER' in os.environ: + sudo_user = os.environ['SUDO_USER'] + import pwd + user_info = pwd.getpwnam(sudo_user) + os.chown(self.report_dir, user_info.pw_uid, user_info.pw_gid) + + def generate_report(self, analysis_data: Dict, current_connection: Optional[Dict] = None) -> str: + """Generate comprehensive HTML report""" + try: + # Extract data components + mesh_analysis = analysis_data.get('mesh_analysis', {}) + alternatives = analysis_data.get('alternatives', []) + historical_data = analysis_data.get('historical_data', {}) + problems = analysis_data.get('problems', {}) + + # Generate the complete HTML content - PASS analysis_data as the 6th parameter + html_content = self._generate_mesh_report( + mesh_analysis, alternatives, current_connection, historical_data, problems, analysis_data + ) + + # Create filename and save + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + filename = f"mesh_analysis_{timestamp}.html" + report_path = self.report_dir / filename + + with open(report_path, 'w', encoding='utf-8') as f: + f.write(html_content) + + return str(report_path) + + except Exception as e: + print(f"❌ Error in HTML reporter: {e}") + import traceback + traceback.print_exc() + return "" + + def _generate_mesh_report(self, mesh_analysis: Dict, alternatives: List[Dict], + current_connection: Optional[Dict], historical_data: Dict, + problems: Dict, analysis_data: Dict) -> str: + """Generate the complete mesh analysis HTML report - NOW WITH analysis_data PARAMETER""" + + # Get additional data from analysis_data - THIS IS WHAT WAS MISSING + roaming_data = analysis_data.get('roaming_data', {}) + power_data = analysis_data.get('power_data', {}) + + # Generate HTML sections + current_connection_html = self._generate_current_connection_section(current_connection) + mesh_topology_html = self._generate_mesh_topology_section(mesh_analysis) + alternatives_html = self._generate_alternatives_section(alternatives, current_connection) + historical_html = self._generate_historical_section(historical_data) + problems_html = self._generate_problems_section(problems) + roaming_html = self._generate_roaming_section(roaming_data) + power_html = self._generate_power_section(power_data) + + # Get page title and summary + if current_connection: + page_title = f"Mesh Analysis - {current_connection.get('ssid', 'Unknown Network')}" + network_name = current_connection.get('ssid', 'Unknown Network') + else: + page_title = "WiFi Mesh Analysis Report" + network_name = "No Active Connection" + + # Generate the complete HTML + html_template = f""" + + + + + {page_title} + + + +
+
+
+

📡 WiFi Mesh Network Analysis

+
+

{network_name}

+

Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}

+
+
+
+ +
+ {current_connection_html} + {mesh_topology_html} + {alternatives_html} + {historical_html} + {problems_html} + {roaming_html} + {power_html} +
+ +
+

📊 Generated by WiFi Mesh Network Analyzer with Venn Overlap Analysis

+

🔍 Analysis includes: Mesh topology, signal optimization, historical tracking, problem detection

+
+
+ + + +""" + + return html_template + + def _generate_current_connection_section(self, current_connection: Optional[Dict]) -> str: + """Generate current connection status section""" + if not current_connection: + return """ +
+

❌ Connection Status

+
+

Not connected to any WiFi network

+
+

💡 Connect to a WiFi network to enable mesh analysis

+
+
+
+ """ + + signal = current_connection.get('signal', -100) + if signal > -50: + signal_class = "excellent" + signal_emoji = "🟢" + elif signal > -60: + signal_class = "good" + signal_emoji = "🟡" + elif signal > -75: + signal_class = "fair" + signal_emoji = "🟠" + else: + signal_class = "poor" + signal_emoji = "🔴" + + band = self._get_band_from_freq(current_connection.get('freq', 0)) + + return f""" +
+

🔗 Current Connection

+
+
+ Network (SSID): + {current_connection.get('ssid', 'Unknown')} +
+
+ Access Point (BSSID): + {current_connection.get('bssid', 'Unknown')} +
+
+ Signal Strength: + {signal_emoji} {signal} dBm +
+
+ Frequency: + {current_connection.get('freq', 0)} MHz ({band}) +
+
+
+ """ + + def _generate_mesh_topology_section(self, mesh_analysis: Dict) -> str: + """Generate mesh topology analysis section""" + if not mesh_analysis: + return """ +
+

🏠 Network Topology

+

No topology analysis available

+
+ """ + + network_type = mesh_analysis.get('type', 'unknown') + + if network_type == 'mesh': + return self._generate_mesh_system_html(mesh_analysis) + elif network_type == 'single_ap': + return self._generate_single_ap_html(mesh_analysis) + elif network_type == 'multiple_aps': + return self._generate_multiple_aps_html(mesh_analysis) + else: + return f""" +
+

🏠 Network Topology

+

Network type: {network_type.replace('_', ' ').title()}

+
+ """ + + def _generate_mesh_system_html(self, mesh_analysis: Dict) -> str: + """Generate HTML for mesh system analysis""" + brand = mesh_analysis.get('brand', 'unknown').replace('_', ' ').title() + mesh_type = mesh_analysis.get('mesh_type', 'unknown').replace('_', '-').title() + total_nodes = mesh_analysis.get('total_nodes', 0) + total_radios = mesh_analysis.get('total_radios', 0) + bands = mesh_analysis.get('bands', []) + + # Get topology health status + topology_health = mesh_analysis.get('topology_health', 'unknown') + coverage_analysis = mesh_analysis.get('coverage_analysis', {}) + quality_score = coverage_analysis.get('coverage_quality_score', 0) + + if topology_health in ['excellent_topology', 'good_topology']: + health_class = "excellent" + health_emoji = "🟢" + elif topology_health == 'basic_topology': + health_class = "good" + health_emoji = "🟡" + else: + health_class = "warning" + health_emoji = "🟠" + + # Generate coverage zones visualization + zones_html = "" + zones = coverage_analysis.get('coverage_zones', {}) + if zones: + zones_html = """ +
+

📍 Coverage Zones

+
+ """ + + for zone_name, signals in zones.items(): + if signals: + zone_emoji = { + 'primary': '🟢', + 'secondary': '🟡', + 'tertiary': '🟠', + 'fringe': '🔴' + }.get(zone_name, '⚪') + + zone_title = zone_name.title() + signal_range = f"{min(signals)} to {max(signals)}dBm" if len(signals) > 1 else f"{signals[0]}dBm" + + zones_html += f""" +
+
+ {zone_emoji} + {zone_title} +
+
+
{len(signals)} nodes
+
{signal_range}
+
+
+ """ + + zones_html += """ +
+
+ """ + + # Generate Venn diagram section - RESTORED WITH VISUAL SVG! + venn_html = "" + venn_analysis = mesh_analysis.get('venn_analysis', {}) + if venn_analysis and venn_analysis.get('venn_diagram'): + venn_html = """ +
+

🔄 Venn Overlap Analysis

+
+ """ + + overlap_quality = venn_analysis.get('overlap_quality', {}) + quality = overlap_quality.get('quality', 'unknown') + score = overlap_quality.get('score', 0) + + quality_class = { + 'excellent': 'excellent', + 'good': 'good', + 'fair': 'warning', + 'poor': 'poor' + }.get(quality, 'poor') + + venn_html += f""" +
+ {score}/100 + Overlap Quality +
+
+

{overlap_quality.get('description', 'No description available')}

+
+
+ """ + + # Generate VISUAL SVG Venn diagram - THIS IS WHAT WAS MISSING! + venn_data = venn_analysis['venn_diagram'] + overlaps = venn_data.get('overlaps', []) + nodes = venn_data.get('nodes', []) + + if nodes and len(nodes) >= 2: + venn_svg = self._generate_venn_svg(nodes, overlaps) + venn_html += f""" +
+
Visual Coverage Overlap:
+
+ {venn_svg} +
+
+ """ + + # Show overlap details + if overlaps: + venn_html += """ +
+
Node Overlap Details:
+ """ + for overlap in overlaps[:5]: # Show top 5 + overlap_pct = overlap.get('overlap_percentage', 0) + venn_html += f""" +
+ {overlap.get('node1_label', 'Node')} ↔ {overlap.get('node2_label', 'Node')} + {overlap_pct:.1f}% +
+ """ + venn_html += "
" + else: + venn_html += """ +
+

⚠️ No significant overlaps detected - potential coverage gaps

+
+ """ + + venn_html += "
" + + # Generate mesh nodes details + nodes_html = "" + mesh_nodes = mesh_analysis.get('mesh_nodes', {}) + if mesh_nodes: + nodes_html = """ +
+

🏠 Detected Mesh Nodes

+
+ """ + + for node_id, node_data in mesh_nodes.items(): + radios = node_data.get('radios', []) + strongest_signal = node_data.get('strongest_signal', -100) + + if strongest_signal > -50: + node_class = "excellent" + elif strongest_signal > -65: + node_class = "good" + elif strongest_signal > -80: + node_class = "fair" + else: + node_class = "poor" + + nodes_html += f""" +
+
+
Node {node_id[-8:]}
+ {strongest_signal}dBm +
+
+ """ + + for radio in radios: + band = self._get_band_from_freq(radio.get('freq', 0)) + nodes_html += f""" +
+ {band} + {radio.get('signal', -100)}dBm +
+ """ + + nodes_html += """ +
+
+ """ + + nodes_html += """ +
+
+ """ + + return f""" +
+

🏠 Mesh Network Topology

+ +
+
+
+ Brand: + {brand} +
+
+ Type: + {mesh_type} Mesh +
+
+ Nodes: + {total_nodes} +
+
+ Radios: + {total_radios} +
+
+ Bands: + {', '.join(bands)} +
+
+ +
+
+ {health_emoji} + Health Score: {quality_score:.0f}/100 +
+
{topology_health.replace('_', ' ').title()}
+
+
+ + {zones_html} + {venn_html} + {nodes_html} + +
+

ℹ️ Only shows nodes visible from your current location. Distant or weak nodes may not appear.

+
+
+ """ + + def _generate_venn_svg(self, nodes: List[Dict], overlaps: List[Dict]) -> str: + """Generate SVG Venn diagram visualization - THE MISSING VISUAL COMPONENT!""" + if len(nodes) < 2: + return "

Need at least 2 nodes for Venn diagram

" + + # SVG dimensions + width = 400 + height = 300 + + # Calculate circle positions and sizes based on signal strength + circles = [] + for i, node in enumerate(nodes[:4]): # Max 4 nodes for visual clarity + signal = node.get('signal', -100) + # Convert signal to radius (stronger signal = larger circle) + radius = max(30, min(80, (100 + signal) * 2)) + + # Position circles in a pattern + if i == 0: + x, y = width * 0.3, height * 0.4 + elif i == 1: + x, y = width * 0.7, height * 0.4 + elif i == 2: + x, y = width * 0.3, height * 0.7 + else: + x, y = width * 0.7, height * 0.7 + + # Color based on signal strength + if signal > -50: + color = "#4ade80" # Green + opacity = "0.6" + elif signal > -65: + color = "#fbbf24" # Yellow + opacity = "0.5" + elif signal > -80: + color = "#fb923c" # Orange + opacity = "0.4" + else: + color = "#f87171" # Red + opacity = "0.3" + + circles.append({ + 'x': x, 'y': y, 'r': radius, + 'color': color, 'opacity': opacity, + 'label': node.get('label', f'Node {i+1}'), + 'signal': signal + }) + + # Generate SVG + svg = f""" + + + + + + + + + + """ + + for circle in circles: + svg += f""" + + """ + + # Add labels + for circle in circles: + svg += f""" + {circle['label']} + {circle['signal']}dBm + """ + + # Add title + svg += f""" + + Mesh Node Coverage Overlap + + + + + Circle size = signal strength + Overlapping areas = good coverage + Colors: Green=Excellent, Yellow=Good, Orange=Fair, Red=Poor + + + """ + + return svg + + def _generate_single_ap_html(self, mesh_analysis: Dict) -> str: + """Generate HTML for single access point""" + signal_quality = mesh_analysis.get('signal_quality', 'unknown') + signal_reason = mesh_analysis.get('signal_reason', 'No analysis available') + + if signal_quality == 'excellent': + quality_class = "excellent" + quality_emoji = "🟢" + elif signal_quality == 'good': + quality_class = "good" + quality_emoji = "🟡" + elif signal_quality == 'fair': + quality_class = "warning" + quality_emoji = "🟠" + else: + quality_class = "poor" + quality_emoji = "🔴" + + return f""" +
+

📡 Single Access Point Network

+ +
+
+ {quality_emoji} + {signal_quality.replace('_', ' ').title()} +
+ +
+

{signal_reason}

+
+
+
+ """ + + def _generate_multiple_aps_html(self, mesh_analysis: Dict) -> str: + """Generate HTML for multiple access points""" + nodes = mesh_analysis.get('nodes', 0) + signal_quality = mesh_analysis.get('signal_quality', 'unknown') + signal_reason = mesh_analysis.get('signal_reason', 'No analysis available') + + if signal_quality == 'excellent': + quality_class = "excellent" + quality_emoji = "🟢" + elif signal_quality == 'good': + quality_class = "good" + quality_emoji = "🟡" + elif signal_quality == 'fair': + quality_class = "warning" + quality_emoji = "🟠" + else: + quality_class = "poor" + quality_emoji = "🔴" + + return f""" +
+

📡 Multiple Access Points

+ +
+
+ {nodes} + Access Points +
+ +
+ {quality_emoji} + {signal_quality.replace('_', ' ').title()} +
+ +
+

{signal_reason}

+
+
+
+ """ + + def _generate_alternatives_section(self, alternatives: List[Dict], current_connection: Optional[Dict]) -> str: + """Generate alternatives analysis section""" + if not alternatives or not current_connection: + return """ +
+

🎯 Connection Alternatives

+
+

✅ Current connection appears optimal or no alternatives available

+
+
+ """ + + # Check if any alternatives are compelling + compelling_alternatives = [alt for alt in alternatives if alt.get('compelling_reason', False)] + + alternatives_html = """ +
+

🎯 Connection Alternatives

+ """ + + if compelling_alternatives: + best = compelling_alternatives[0] + alternatives_html += f""" +
+
+ 💡 + Performance Optimization Opportunity +
+
+

Recommended BSSID: {best['bssid']}

+

Expected Improvement: {best['signal_diff']:+d}dB signal strength

+

Quality Rating: {best['recommendation']}

+
+
+ """ + else: + alternatives_html += """ +
+
+ ✅ + Current Connection is Optimal +
+
+

Your current connection is performing well among available options.

+
+
+ """ + + # Show top alternatives + alternatives_html += """ +
+

📊 Available Options

+
+ """ + + for i, alt in enumerate(alternatives[:3], 1): + band = self._get_band_from_freq(alt.get('freq', 0)) + + if alt['recommendation'] == 'EXCELLENT': + alt_class = "excellent" + alt_emoji = "🟢" + elif alt['recommendation'] == 'GOOD': + alt_class = "good" + alt_emoji = "🟡" + elif alt['recommendation'] == 'FAIR': + alt_class = "fair" + alt_emoji = "🟠" + else: + alt_class = "poor" + alt_emoji = "🔴" + + reasons_html = "" + for reason in alt.get('reasons', []): + reasons_html += f"
  • {reason}
  • " + + alternatives_html += f""" +
    +
    + {alt_emoji} + Option {i} + {alt['recommendation']} +
    +
    +
    + BSSID: + {alt['bssid']} +
    +
    + Signal: + {alt['signal']}dBm ({band}) +
    +
    + Difference: + {alt['signal_diff']:+d}dB +
    +
    + Score: + {alt['score']:.0f}/100 +
    +
    +
    +
      {reasons_html}
    +
    +
    + """ + + alternatives_html += """ +
    +
    +
    + """ + + return alternatives_html + + def _generate_historical_section(self, historical_data: Dict) -> str: + """Generate historical performance section""" + if not historical_data: + return """ +
    +

    📈 Historical Performance

    +
    +

    📊 No historical data available for current connection

    +

    🕐 Data will be collected over time for performance tracking

    +
    +
    + """ + + stability_score = historical_data.get('stability_score', 0) + success_rate = historical_data.get('success_rate', 0) + total_connections = historical_data.get('total_connections', 0) + avg_signal = historical_data.get('avg_signal', 0) + + if stability_score >= 90: + stability_class = "excellent" + stability_emoji = "🟢" + elif stability_score >= 75: + stability_class = "good" + stability_emoji = "🟡" + elif stability_score >= 60: + stability_class = "fair" + stability_emoji = "🟠" + else: + stability_class = "poor" + stability_emoji = "🔴" + + return f""" +
    +

    📈 Historical Performance

    + +
    +
    + {stability_emoji} +
    + {stability_score:.1f}/100 + Stability Score +
    +
    + +
    +
    + {success_rate:.1f}% + Success Rate +
    +
    + {total_connections} + Total Connections +
    +
    + {avg_signal:.0f}dBm + Avg Signal +
    +
    +
    + +
    +

    📊 Performance data collected over time from actual usage patterns

    +
    +
    + """ + + def _generate_problems_section(self, problems: Dict) -> str: + """Generate problems detection section""" + if not problems: + return """ +
    +

    🚨 Problem Detection

    +
    +

    ✅ No problematic patterns detected in recent activity

    +
    +
    + """ + + total_issues = ( + len(problems.get('roaming_loops', [])) + + len(problems.get('auth_failure_clusters', [])) + + len(problems.get('rapid_disconnects', [])) + ) + + if total_issues == 0: + return """ +
    +

    🚨 Problem Detection

    +
    +

    ✅ No problematic patterns detected in recent activity

    +
    +
    + """ + + problems_html = f""" +
    +

    🚨 Problem Detection

    + +
    +
    + {total_issues} + Issues Detected +
    +
    + +
    + """ + + if problems.get('roaming_loops'): + problems_html += f""" +
    + 🔄 + Roaming Loops: {len(problems['roaming_loops'])} +
    + """ + + if problems.get('auth_failure_clusters'): + problems_html += f""" +
    + 🔐 + Auth Failure Clusters: {len(problems['auth_failure_clusters'])} +
    + """ + + if problems.get('rapid_disconnects'): + problems_html += f""" +
    + ⚡ + Rapid Reconnects: {len(problems['rapid_disconnects'])} +
    + """ + + problems_html += """ +
    +
    + """ + + return problems_html + + def _generate_roaming_section(self, roaming_data: Dict) -> str: + """Generate roaming analysis section""" + if not roaming_data: + return """ +
    +

    🚶 Roaming Analysis

    +
    +

    📊 No roaming analysis data available

    +

    💡 Use --roaming-test to generate roaming performance data

    +
    +
    + """ + + return f""" +
    +

    🚶 Roaming Analysis

    +
    +

    🔍 Roaming analysis data available

    +
    {json.dumps(roaming_data, indent=2)}
    +
    +
    + """ + + def _generate_power_section(self, power_data: Dict) -> str: + """Generate power management section""" + if not power_data or not power_data.get('issues_found'): + return """ +
    +

    🔋 Power Management

    +
    +

    ✅ No power management issues detected

    +
    +
    + """ + + severity_counts = power_data.get('severity_counts', {}) + total_issues = power_data.get('total_issues', 0) + + return f""" +
    +

    🔋 Power Management Issues

    + +
    +
    + {total_issues} + Issues Found +
    + +
    +
    + {severity_counts.get('high', 0)} + High +
    +
    + {severity_counts.get('medium', 0)} + Medium +
    +
    + {severity_counts.get('low', 0)} + Low +
    +
    +
    + +
    +

    💡 These issues require manual configuration changes

    +
    +
    + """ + + def _get_band_from_freq(self, freq: int) -> str: + """Get band name from frequency""" + if 2400 <= freq <= 2500: + return '2.4GHz' + elif 5000 <= freq <= 5999: + return '5GHz' + elif 6000 <= freq <= 7125: + return '6GHz' + else: + return f'{freq}MHz' + + def _get_modern_css(self) -> str: + """Return modern CSS with dark theme and glassmorphism""" + return """ + * { + margin: 0; + padding: 0; + box-sizing: border-box; + } + + body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', sans-serif; + background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%); + color: #ffffff; + min-height: 100vh; + line-height: 1.6; + } + + .container { + max-width: 1400px; + margin: 0 auto; + padding: 20px; + } + + .main-header { + background: rgba(255, 255, 255, 0.05); + backdrop-filter: blur(20px); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 20px; + padding: 30px; + margin-bottom: 30px; + text-align: center; + } + + .main-header h1 { + font-size: 2.5rem; + font-weight: 700; + margin-bottom: 15px; + background: linear-gradient(135deg, #00d4ff, #7b68ee); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + } + + .wifi-icon { + font-size: 2rem; + margin-right: 15px; + } + + .network-info h2 { + font-size: 1.5rem; + color: #00d4ff; + margin-bottom: 10px; + } + + .timestamp { + color: rgba(255, 255, 255, 0.7); + font-size: 0.9rem; + } + + .content-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); + gap: 25px; + margin-bottom: 30px; + } + + .card { + background: rgba(255, 255, 255, 0.05); + backdrop-filter: blur(20px); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 20px; + padding: 25px; + transition: all 0.3s ease; + } + + .card:hover { + transform: translateY(-5px); + border-color: rgba(0, 212, 255, 0.3); + box-shadow: 0 20px 40px rgba(0, 212, 255, 0.1); + } + + .card h3 { + font-size: 1.3rem; + font-weight: 600; + margin-bottom: 20px; + color: #00d4ff; + display: flex; + align-items: center; + } + + .icon { + font-size: 1.5rem; + margin-right: 10px; + } + + .connection-details, .topology-stats { + display: flex; + flex-direction: column; + gap: 12px; + } + + .detail-row, .stat-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px 15px; + background: rgba(255, 255, 255, 0.03); + border-radius: 10px; + border: 1px solid rgba(255, 255, 255, 0.05); + } + + .label, .stat-label { + color: rgba(255, 255, 255, 0.8); + font-weight: 500; + } + + .value, .stat-value { + font-weight: 600; + color: #ffffff; + } + + .signal-excellent { color: #4ade80; } + .signal-good { color: #fbbf24; } + .signal-fair { color: #fb923c; } + .signal-poor { color: #f87171; } + + .excellent { + border-color: rgba(74, 222, 128, 0.3); + background: rgba(74, 222, 128, 0.05); + } + + .good { + border-color: rgba(251, 191, 36, 0.3); + background: rgba(251, 191, 36, 0.05); + } + + .warning, .fair { + border-color: rgba(251, 146, 60, 0.3); + background: rgba(251, 146, 60, 0.05); + } + + .poor { + border-color: rgba(248, 113, 113, 0.3); + background: rgba(248, 113, 113, 0.05); + } + + .topology-overview { + display: grid; + grid-template-columns: 2fr 1fr; + gap: 20px; + margin-bottom: 25px; + } + + .topology-health { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 20px; + border-radius: 15px; + text-align: center; + } + + .health-score { + display: flex; + align-items: center; + gap: 10px; + font-size: 1.1rem; + font-weight: 600; + margin-bottom: 8px; + } + + .health-emoji { + font-size: 1.5rem; + } + + .coverage-zones, .mesh-nodes, .venn-analysis { + margin-top: 25px; + } + + .coverage-zones h4, .mesh-nodes h4, .venn-analysis h4 { + color: #00d4ff; + margin-bottom: 15px; + font-size: 1.1rem; + } + + .venn-summary { + display: flex; + align-items: center; + gap: 20px; + margin-bottom: 20px; + padding: 15px; + background: rgba(255, 255, 255, 0.03); + border-radius: 10px; + border: 1px solid rgba(255, 255, 255, 0.1); + } + + .venn-quality { + display: flex; + flex-direction: column; + align-items: center; + padding: 15px; + border-radius: 10px; + min-width: 80px; + } + + .quality-score { + font-size: 1.5rem; + font-weight: 700; + color: #fff; + } + + .quality-label { + font-size: 0.8rem; + color: rgba(255, 255, 255, 0.8); + } + + .venn-description { + flex: 1; + color: rgba(255, 255, 255, 0.9); + } + + .venn-diagram-container { + margin-top: 20px; + padding: 15px; + background: rgba(255, 255, 255, 0.03); + border-radius: 10px; + border: 1px solid rgba(255, 255, 255, 0.1); + } + + .venn-diagram-container h5 { + color: #00d4ff; + margin-bottom: 15px; + font-size: 1rem; + } + + .venn-svg-wrapper { + display: flex; + justify-content: center; + align-items: center; + background: rgba(255, 255, 255, 0.9); + border-radius: 10px; + padding: 10px; + margin-bottom: 15px; + } + + .venn-svg-wrapper svg { + max-width: 100%; + height: auto; + border-radius: 8px; + } + + .overlap-list { + display: flex; + flex-direction: column; + gap: 10px; + } + + .overlap-list h5 { + color: #00d4ff; + margin-bottom: 10px; + font-size: 1rem; + } + + .overlap-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px 15px; + background: rgba(255, 255, 255, 0.03); + border-radius: 8px; + border: 1px solid rgba(255, 255, 255, 0.05); + } + + .overlap-nodes { + color: rgba(255, 255, 255, 0.9); + } + + .overlap-percentage { + font-weight: 600; + color: #00d4ff; + } + + .no-overlaps { + padding: 20px; + text-align: center; + background: rgba(251, 146, 60, 0.05); + border: 1px solid rgba(251, 146, 60, 0.2); + border-radius: 10px; + color: rgba(255, 255, 255, 0.8); + } + + .zones-grid, .nodes-grid, .alternatives-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 15px; + } + + .zone-card, .node-card, .alternative-card { + background: rgba(255, 255, 255, 0.03); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 12px; + padding: 15px; + transition: all 0.2s ease; + } + + .zone-card:hover, .node-card:hover, .alternative-card:hover { + background: rgba(255, 255, 255, 0.06); + border-color: rgba(0, 212, 255, 0.3); + } + + .zone-header, .node-header, .alt-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 10px; + } + + .zone-icon, .alt-emoji { + font-size: 1.2rem; + } + + .recommendation-banner { + padding: 20px; + border-radius: 15px; + margin-bottom: 25px; + border: 1px solid rgba(255, 255, 255, 0.1); + } + + .recommendation-header { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 12px; + } + + .rec-icon { + font-size: 1.5rem; + } + + .rec-title { + font-size: 1.2rem; + font-weight: 600; + } + + .recommendation-details { + color: rgba(255, 255, 255, 0.9); + } + + .alternatives-list h4 { + color: #00d4ff; + margin-bottom: 15px; + font-size: 1.1rem; + } + + .alt-details { + display: flex; + flex-direction: column; + gap: 8px; + margin-bottom: 12px; + } + + .alt-stat { + display: flex; + justify-content: space-between; + font-size: 0.9rem; + } + + .alt-reasons ul { + list-style: none; + font-size: 0.85rem; + color: rgba(255, 255, 255, 0.8); + } + + .alt-reasons li { + margin-bottom: 4px; + padding-left: 8px; + position: relative; + } + + .alt-reasons li::before { + content: "•"; + color: #00d4ff; + position: absolute; + left: 0; + } + + .stability-score { + display: flex; + align-items: center; + gap: 15px; + padding: 20px; + border-radius: 15px; + margin-bottom: 20px; + } + + .stability-details { + display: flex; + flex-direction: column; + } + + .stability-number { + font-size: 1.5rem; + font-weight: 700; + } + + .stability-label { + font-size: 0.9rem; + color: rgba(255, 255, 255, 0.8); + } + + .history-stats { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 15px; + } + + .history-stat { + text-align: center; + padding: 15px; + background: rgba(255, 255, 255, 0.03); + border-radius: 10px; + border: 1px solid rgba(255, 255, 255, 0.05); + } + + .stat-number { + display: block; + font-size: 1.3rem; + font-weight: 700; + color: #00d4ff; + } + + .problems-summary, .power-summary { + display: flex; + align-items: center; + gap: 30px; + margin-bottom: 20px; + } + + .problems-count, .issues-count, .count-number { + text-align: center; + } + + .count-number { + display: block; + font-size: 2rem; + font-weight: 700; + color: #fb923c; + } + + .count-label { + font-size: 0.9rem; + color: rgba(255, 255, 255, 0.8); + } + + .problems-list { + display: flex; + flex-direction: column; + gap: 10px; + } + + .problem-item { + display: flex; + align-items: center; + gap: 12px; + padding: 12px 15px; + background: rgba(255, 255, 255, 0.03); + border-radius: 8px; + border: 1px solid rgba(251, 146, 60, 0.2); + } + + .problem-icon { + font-size: 1.2rem; + } + + .severity-breakdown { + display: flex; + gap: 15px; + } + + .severity-item { + text-align: center; + padding: 10px 15px; + border-radius: 8px; + border: 1px solid rgba(255, 255, 255, 0.1); + } + + .severity-item.high { + border-color: rgba(248, 113, 113, 0.3); + background: rgba(248, 113, 113, 0.05); + } + + .severity-item.medium { + border-color: rgba(251, 146, 60, 0.3); + background: rgba(251, 146, 60, 0.05); + } + + .severity-item.low { + border-color: rgba(251, 191, 36, 0.3); + background: rgba(251, 191, 36, 0.05); + } + + .severity-number { + display: block; + font-size: 1.2rem; + font-weight: 600; + } + + .severity-label { + font-size: 0.8rem; + color: rgba(255, 255, 255, 0.8); + } + + .topology-note, .history-note, .power-note { + margin-top: 20px; + padding: 15px; + background: rgba(0, 212, 255, 0.05); + border: 1px solid rgba(0, 212, 255, 0.2); + border-radius: 10px; + font-size: 0.9rem; + color: rgba(255, 255, 255, 0.9); + } + + .no-connection, .no-alternatives, .no-history, .no-problems, .no-roaming-data, .no-power-issues { + text-align: center; + padding: 30px; + color: rgba(255, 255, 255, 0.8); + } + + .report-footer { + text-align: center; + padding: 25px; + background: rgba(255, 255, 255, 0.02); + border-radius: 15px; + border: 1px solid rgba(255, 255, 255, 0.05); + color: rgba(255, 255, 255, 0.7); + font-size: 0.9rem; + } + + .report-footer p { + margin-bottom: 5px; + } + + pre { + background: rgba(0, 0, 0, 0.3); + padding: 15px; + border-radius: 8px; + overflow-x: auto; + font-size: 0.8rem; + border: 1px solid rgba(255, 255, 255, 0.1); + } + + @media (max-width: 768px) { + .container { + padding: 15px; + } + + .content-grid { + grid-template-columns: 1fr; + gap: 20px; + } + + .topology-overview { + grid-template-columns: 1fr; + } + + .zones-grid, .nodes-grid, .alternatives-grid { + grid-template-columns: 1fr; + } + + .main-header h1 { + font-size: 2rem; + } + + .history-stats { + grid-template-columns: 1fr; + } + + .problems-summary, .power-summary { + flex-direction: column; + gap: 15px; + } + + .severity-breakdown { + justify-content: center; + } + } + """ + + def _get_interactive_javascript(self) -> str: + """Return interactive JavaScript for the report""" + return """ + // Add smooth scrolling and interactive features + document.addEventListener('DOMContentLoaded', function() { + // Add click-to-copy functionality for BSSIDs + const bssids = document.querySelectorAll('.stat-value, .value'); + bssids.forEach(element => { + if (element.textContent.match(/[0-9A-Fa-f]{2}:[0-9A-Fa-f]{2}:[0-9A-Fa-f]{2}:[0-9A-Fa-f]{2}:[0-9A-Fa-f]{2}:[0-9A-Fa-f]{2}/)) { + element.style.cursor = 'pointer'; + element.title = 'Click to copy BSSID'; + element.addEventListener('click', function() { + navigator.clipboard.writeText(this.textContent).then(() => { + const original = this.textContent; + this.textContent = '✓ Copied!'; + setTimeout(() => { + this.textContent = original; + }, 1000); + }); + }); + } + }); + + // Add smooth hover effects + const cards = document.querySelectorAll('.card'); + cards.forEach(card => { + card.addEventListener('mouseenter', function() { + this.style.transform = 'translateY(-5px) scale(1.02)'; + }); + card.addEventListener('mouseleave', function() { + this.style.transform = 'translateY(0) scale(1)'; + }); + }); + + // Auto-refresh timestamp + const timestamp = document.querySelector('.timestamp'); + if (timestamp) { + setInterval(() => { + const now = new Date(); + const timeStr = now.toLocaleString(); + timestamp.textContent = 'Generated: ' + timeStr + ' (Auto-updated)'; + }, 60000); // Update every minute + } + }); + """ + + +# Example usage and integration +if __name__ == "__main__": + print("WiFi Mesh HTML Report Generator") + print("This module provides HTML reporting for the WiFi Mesh Network Analyzer") + print("Usage: Import MeshHTMLReporter and call generate_report() with analysis data") diff --git a/MeshAnalyzer/files/mesh_power_detective.py b/MeshAnalyzer/files/mesh_power_detective.py new file mode 100644 index 0000000..7065f82 --- /dev/null +++ b/MeshAnalyzer/files/mesh_power_detective.py @@ -0,0 +1,648 @@ +#!/usr/bin/env python3 +""" +WiFi Mesh Power Detective +Detect WiFi power management issues causing false disconnects +Save this file as: mesh_power_detective.py +Put it in the same folder as your main analyzer script +""" + +import subprocess +import time +import os +import re +import glob +from datetime import datetime +from pathlib import Path + +class MeshPowerDetective: + """Detect WiFi power management issues causing false disconnects""" + + def __init__(self, interface): + self.interface = interface + self.issues_found = [] + self.power_events = [] + self.alert_thresholds = { + 'signal_drop': 15, # dBm + 'latency_spike': 100, # ms + 'packet_loss': 5 # percent + } + + def check_all_power_issues(self): + """Run comprehensive power management detection""" + print("🔋 WiFi Power Management Detective") + print("=" * 60) + print("Scanning for power-related WiFi issues...") + + issues = { + 'wifi_power_save': self.check_wifi_power_save(), + 'usb_autosuspend': self.check_usb_autosuspend(), + 'pcie_aspm': self.check_pcie_aspm(), + 'network_manager': self.check_network_manager_power(), + 'tlp_settings': self.check_tlp_settings(), + 'laptop_mode': self.check_laptop_mode_tools(), + 'systemd_sleep': self.check_systemd_sleep_settings(), + 'driver_params': self.check_driver_power_params() + } + + self._generate_report(issues) + return issues + + def check_wifi_power_save(self): + """Check if WiFi power saving is causing drops""" + issues = [] + + # Check current power save status + try: + result = subprocess.run(f"iw dev {self.interface} get power_save", + shell=True, capture_output=True, text=True, timeout=5) + if "on" in result.stdout.lower(): + issues.append({ + 'severity': 'high', + 'issue': 'WiFi power saving is ON', + 'impact': 'Can cause periodic disconnects and latency spikes', + 'fix': f'sudo iw dev {self.interface} set power_save off' + }) + except: + pass + + return issues + + def check_usb_autosuspend(self): + """Check if USB autosuspend is affecting USB WiFi adapters""" + issues = [] + + # Check if WiFi is USB + try: + # Find device path + device_path = f"/sys/class/net/{self.interface}/device" + if os.path.exists(device_path): + real_path = os.path.realpath(device_path) + + if "usb" in real_path: + # It's a USB WiFi adapter + issues.append({ + 'severity': 'info', + 'issue': 'USB WiFi adapter detected', + 'impact': 'USB autosuspend can cause disconnects' + }) + + # Check USB autosuspend setting + autosuspend_path = "/sys/module/usbcore/parameters/autosuspend" + if os.path.exists(autosuspend_path): + with open(autosuspend_path, 'r') as f: + value = f.read().strip() + if value != "-1": + issues.append({ + 'severity': 'high', + 'issue': f'USB autosuspend is enabled ({value}s)', + 'impact': 'WiFi adapter suspends after inactivity', + 'fix': 'echo -1 | sudo tee /sys/module/usbcore/parameters/autosuspend' + }) + + # Check specific device power/control + usb_power_path = None + for parent in Path(real_path).parents: + control_path = parent / "power/control" + if control_path.exists(): + usb_power_path = control_path + break + + if usb_power_path and usb_power_path.exists(): + with open(usb_power_path, 'r') as f: + if f.read().strip() == "auto": + issues.append({ + 'severity': 'high', + 'issue': 'USB device power control set to auto', + 'impact': 'Device can sleep during use', + 'fix': f'echo on | sudo tee {usb_power_path}' + }) + except Exception as e: + # Debug info for troubleshooting + pass + + return issues + + def check_pcie_aspm(self): + """Check PCIe Active State Power Management""" + issues = [] + + # Check if WiFi is PCIe + try: + result = subprocess.run(f"lspci -k | grep -A 3 -i network", + shell=True, capture_output=True, text=True, timeout=5) + if result.stdout: + # Check ASPM policy + aspm_path = "/sys/module/pcie_aspm/parameters/policy" + if os.path.exists(aspm_path): # FIXED: was "asmp_path" + with open(aspm_path, 'r') as f: # FIXED: was "asmp_path" + policy = f.read().strip() + if policy[policy.find('[')+1:policy.find(']')] in ['default', 'powersave', 'powersupersave']: + issues.append({ + 'severity': 'medium', + 'issue': f'PCIe ASPM set to: {policy}', + 'impact': 'Can cause latency and brief disconnects', + 'fix': 'Add pcie_aspm=off to kernel boot parameters', + 'manual_only': True + }) + except: + pass + + return issues + + def check_network_manager_power(self): + """Check NetworkManager power saving settings""" + issues = [] + + try: + # Check if NetworkManager is managing power + nm_conf_paths = [f for f in glob.glob("/etc/NetworkManager/conf.d/*.conf") if os.path.isfile(f)] + + for conf_path in nm_conf_paths: + if os.path.exists(conf_path): + with open(conf_path, 'r') as f: + content = f.read() + if "wifi.powersave = 3" in content or "wifi.powersave = 1" in content or "wifi.powersave = 0" in content: + issues.append({ + 'severity': 'high', + 'issue': 'NetworkManager WiFi power saving enabled', + 'impact': 'Periodic disconnects and poor roaming', + 'fix': 'Set wifi.powersave = 2 (disable) in ' + conf_path + }) + except: + pass + + return issues + + def check_tlp_settings(self): + """Check TLP (laptop power management) settings""" + issues = [] + + if os.path.exists("/etc/tlp.conf"): + try: + with open("/etc/tlp.conf", 'r') as f: + content = f.read() + + # Check WiFi power saving + if re.search(r'WIFI_PWR_ON_AC\s*=\s*on', content): + issues.append({ + 'severity': 'medium', + 'issue': 'TLP WiFi power saving on AC', + 'impact': 'Power saving even when plugged in', + 'fix': 'Set WIFI_PWR_ON_AC=off in /etc/tlp.conf' + }) + + # Check USB autosuspend + if re.search(r'USB_AUTOSUSPEND\s*=\s*1', content): + issues.append({ + 'severity': 'high', + 'issue': 'TLP USB autosuspend enabled', + 'impact': 'USB WiFi adapters will suspend', + 'fix': 'Set USB_AUTOSUSPEND=0 in /etc/tlp.conf' + }) + except: + pass + + return issues + + def check_laptop_mode_tools(self): + """Check laptop-mode-tools settings""" + issues = [] + + lmt_conf = "/etc/laptop-mode/conf.d/wireless-power.conf" + if os.path.exists(lmt_conf): + try: + with open(lmt_conf, 'r') as f: + content = f.read() + if 'WIRELESS_AC_POWER_SAVING=1' in content: + issues.append({ + 'severity': 'medium', + 'issue': 'Laptop-mode-tools WiFi power saving on AC', + 'impact': 'Unnecessary power saving when plugged in', + 'fix': 'Set WIRELESS_AC_POWER_SAVING=0 in ' + lmt_conf + }) + except: + pass + + return issues + + def check_systemd_sleep_settings(self): + """Check systemd sleep/suspend settings affecting WiFi""" + issues = [] + + try: + # Check if system is suspending network + result = subprocess.run("systemctl status systemd-networkd", + shell=True, capture_output=True, text=True, timeout=5) + + # Check sleep.conf + if os.path.exists("/etc/systemd/sleep.conf"): + with open("/etc/systemd/sleep.conf", 'r') as f: + content = f.read() + if "HibernateDelaySec=2" in content: + issues.append({ + 'severity': 'low', + 'issue': 'Quick hibernate delay detected', + 'impact': 'System may hibernate network too quickly', + 'fix': 'Increase HibernateDelaySec in /etc/systemd/sleep.conf' + }) + except: + pass + + return issues + + def check_driver_power_params(self): + """Check WiFi driver-specific power parameters""" + issues = [] + + try: + # Get driver name + driver_path = f"/sys/class/net/{self.interface}/device/driver" + if os.path.exists(driver_path): + driver = os.path.basename(os.readlink(driver_path)) + + # Intel WiFi (iwlwifi) + if driver == "iwlwifi": + issues.extend(self._check_intel_power(driver)) + + # Realtek (rtw88, rtw89) + elif "rtw" in driver or "r8" in driver: + issues.extend(self._check_realtek_power(driver)) + + # Atheros/Qualcomm (ath9k, ath10k, ath11k) + elif "ath" in driver: + issues.extend(self._check_atheros_power(driver)) + + # MediaTek (mt76, mt7921, mt7922, etc) + elif "mt7" in driver or "mt76" in driver: + issues.extend(self._check_mediatek_power(driver)) + + # Qualcomm mobile (qca_cld3_wlan) + elif "qca" in driver: + issues.extend(self._check_qualcomm_power(driver)) + + # Marvell (mwifiex, mwl8k) + elif "mwifiex" in driver or "mwl" in driver: + issues.extend(self._check_marvell_power(driver)) + + # Generic power management check for any driver + issues.extend(self._check_generic_power_management()) + + except Exception as e: + pass + + return issues + + def _check_intel_power(self, driver): + """Check Intel WiFi power settings""" + issues = [] + + # Check module parameters + if os.path.exists("/sys/module/iwlwifi/parameters/power_save"): + with open("/sys/module/iwlwifi/parameters/power_save", 'r') as f: + if f.read().strip() == "Y": + issues.append({ + 'severity': 'high', + 'issue': 'Intel WiFi power_save enabled', + 'impact': 'Causes disconnects and poor performance', + 'fix': 'Add iwlwifi.power_save=0 to kernel parameters' + }) + + if os.path.exists("/sys/module/iwlwifi/parameters/power_level"): + with open("/sys/module/iwlwifi/parameters/power_level", 'r') as f: + level = f.read().strip() + if level != "0": + issues.append({ + 'severity': 'medium', + 'issue': f'Intel WiFi power_level={level}', + 'impact': 'Reduced performance for power saving', + 'fix': 'Add iwlwifi.power_level=0 to kernel parameters' + }) + + return issues + + def _check_realtek_power(self, driver): + """Check Realtek WiFi power settings""" + issues = [] + + if os.path.exists(f"/sys/module/{driver}/parameters/disable_lps"): + with open(f"/sys/module/{driver}/parameters/disable_lps", 'r') as f: + if f.read().strip() == "N": + issues.append({ + 'severity': 'high', + 'issue': 'Realtek WiFi LPS (power save) enabled', + 'impact': 'Known to cause frequent disconnects', + 'fix': f'Add {driver}.disable_lps=1 to kernel parameters' + }) + + return issues + + def _check_atheros_power(self, driver): + """Check Atheros WiFi power settings""" + issues = [] + + if os.path.exists(f"/sys/module/{driver}/parameters/ps_enable"): + with open(f"/sys/module/{driver}/parameters/ps_enable", 'r') as f: + if f.read().strip() == "1": + issues.append({ + 'severity': 'medium', + 'issue': 'Atheros WiFi power save enabled', + 'impact': 'May cause latency and disconnects', + 'fix': f'Add {driver}.ps_enable=0 to kernel parameters' + }) + + return issues + + def _check_mediatek_power(self, driver): + """Check MediaTek WiFi power settings""" + issues = [] + + # MT7921/MT7922 (common in newer laptops) + if driver in ["mt7921e", "mt7921u", "mt7922"]: + # Check runtime PM + runtime_pm_path = f"/sys/class/net/{self.interface}/device/power/runtime_status" + if os.path.exists(runtime_pm_path): + with open(runtime_pm_path, 'r') as f: + status = f.read().strip() + if status == "suspended": + issues.append({ + 'severity': 'high', + 'issue': 'MediaTek WiFi runtime suspended', + 'impact': 'Device is currently suspended - will cause drops', + 'fix': f'echo on > /sys/class/net/{self.interface}/device/power/control' + }) + + # Check deep sleep mode + if os.path.exists(f"/sys/module/{driver}/parameters/disable_deep_sleep"): + with open(f"/sys/module/{driver}/parameters/disable_deep_sleep", 'r') as f: + if f.read().strip() == "N": + issues.append({ + 'severity': 'high', + 'issue': 'MediaTek deep sleep enabled', + 'impact': 'Causes 1-3 second reconnection delays', + 'fix': f'echo Y > /sys/module/{driver}/parameters/disable_deep_sleep' + }) + + # MT76 series power management + if "mt76" in driver: + # Check power save mode via debugfs + ps_path = f"/sys/kernel/debug/ieee80211/phy*/netdev:{self.interface}/mt76/runtime-pm" + for path in glob.glob(ps_path): + if os.path.exists(path): + with open(path, 'r') as f: + if "enable" in f.read(): + issues.append({ + 'severity': 'medium', + 'issue': 'MT76 runtime PM enabled', + 'impact': 'May cause latency spikes', + 'fix': 'Disable via debugfs or module parameter' + }) + + return issues + + def _check_qualcomm_power(self, driver): + """Check Qualcomm/QCA WiFi power settings""" + issues = [] + + # QCA6174/QCA9377 (common in laptops) + if driver in ["ath10k_pci", "ath11k_pci"]: + # Check WoWLAN (Wake on WLAN) + wowlan_path = f"/sys/class/net/{self.interface}/phy80211/wowlan" + if os.path.exists(wowlan_path): + with open(wowlan_path, 'r') as f: + if "enabled" in f.read(): + issues.append({ + 'severity': 'medium', + 'issue': 'QCA WoWLAN enabled', + 'impact': 'Can cause false wakeups and power issues', + 'fix': f'iw phy phy0 wowlan disable' + }) + + # Check firmware power save + if os.path.exists(f"/sys/module/{driver}/parameters/fw_powersave"): + with open(f"/sys/module/{driver}/parameters/fw_powersave", 'r') as f: + if f.read().strip() == "1": + issues.append({ + 'severity': 'high', + 'issue': 'QCA firmware power save enabled', + 'impact': 'Known to cause disconnects on QCA chips', + 'fix': f'echo 0 > /sys/module/{driver}/parameters/fw_powersave' + }) + + # Mobile Qualcomm chips + elif driver == "qca_cld3_wlan": + # Check IPA (power aggregator) + if os.path.exists("/sys/module/wlan/parameters/enable_ipa"): + with open("/sys/module/wlan/parameters/enable_ipa", 'r') as f: + if f.read().strip() == "1": + issues.append({ + 'severity': 'low', + 'issue': 'QCA IPA power aggregation enabled', + 'impact': 'May affect throughput for power saving', + 'fix': 'Add wlan.enable_ipa=0 to kernel parameters' + }) + + return issues + + def _check_marvell_power(self, driver): + """Check Marvell WiFi power settings""" + issues = [] + + if driver == "mwifiex": + # Check PS mode + ps_mode_path = f"/sys/kernel/debug/mwifiex/{self.interface}/ps_mode" + if os.path.exists(ps_mode_path): + with open(ps_mode_path, 'r') as f: + mode = f.read().strip() + if mode != "0": + issues.append({ + 'severity': 'high', + 'issue': f'Marvell PS mode {mode} active', + 'impact': 'Aggressive power saving causes drops', + 'fix': f'echo 0 > {ps_mode_path}' + }) + + # Check sleep parameters + if os.path.exists(f"/sys/module/{driver}/parameters/auto_ds"): + with open(f"/sys/module/{driver}/parameters/auto_ds", 'r') as f: + if f.read().strip() == "Y": + issues.append({ + 'severity': 'medium', + 'issue': 'Marvell auto deep sleep enabled', + 'impact': 'Wake-up delays after idle', + 'fix': f'Add {driver}.auto_ds=N to kernel parameters' + }) + + return issues + + def _check_generic_power_management(self): + """Generic power checks that apply to any WiFi driver""" + issues = [] + + # Check runtime PM for ANY WiFi device + runtime_pm = f"/sys/class/net/{self.interface}/device/power/control" + if os.path.exists(runtime_pm): + with open(runtime_pm, 'r') as f: + if f.read().strip() == "auto": + issues.append({ + 'severity': 'medium', + 'issue': 'Generic runtime PM set to auto', + 'impact': 'Device may suspend unexpectedly', + 'fix': f'echo on > {runtime_pm}' + }) + + # Check for aggressive kernel power settings + if os.path.exists("/proc/sys/kernel/nmi_watchdog"): + with open("/proc/sys/kernel/nmi_watchdog", 'r') as f: + if f.read().strip() == "0": + issues.append({ + 'severity': 'info', + 'issue': 'NMI watchdog disabled (laptop power saving)', + 'impact': 'System may be in aggressive power save mode', + 'fix': 'Consider if other subsystems are also affected' + }) + + return issues + + def monitor_power_events(self, duration=60): + """Monitor for power-related WiFi events""" + print(f"\n🔍 Monitoring for power-related issues for {duration} seconds...") + print("Watch for correlation between power events and disconnects") + + start_time = time.time() + last_state = "unknown" + + while time.time() - start_time < duration: + # Check power save state + try: + result = subprocess.run(f"iw dev {self.interface} get power_save", + shell=True, capture_output=True, text=True, timeout=2) + current_state = "on" if "on" in result.stdout.lower() else "off" + + if current_state != last_state and last_state != "unknown": + self.power_events.append({ + 'time': datetime.now(), + 'event': f'Power save changed: {last_state} -> {current_state}' + }) + print(f"⚡ Power save state changed to: {current_state}") + + last_state = current_state + + # Check connection state + link_result = subprocess.run(f"iw dev {self.interface} link", + shell=True, capture_output=True, text=True, timeout=2) + if "Not connected" in link_result.stdout: + self.power_events.append({ + 'time': datetime.now(), + 'event': 'Connection lost (check if power-related)' + }) + + except: + pass + + time.sleep(0.5) + + if self.power_events: + print(f"\n📊 Detected {len(self.power_events)} power-related events") + else: + print("\n✅ No power-related events detected") + + def _generate_report(self, issues): + """Generate comprehensive report""" + total_issues = sum(len(v) for v in issues.values()) + critical_issues = sum(1 for v in issues.values() for i in v if i.get('severity') == 'high') + + print(f"\n📋 POWER MANAGEMENT REPORT") + print("=" * 60) + print(f"Total issues found: {total_issues}") + print(f"Critical issues: {critical_issues}") + + if total_issues == 0: + print("\n✅ No power management issues detected!") + print("Your WiFi should not be affected by power saving.") + else: + print("\n🚨 Issues Found:\n") + + for category, category_issues in issues.items(): + if category_issues: + print(f"{category.replace('_', ' ').title()}:") + for issue in category_issues: + severity_icon = { + 'high': '🔴', + 'medium': '🟡', + 'low': '🟠', + 'info': 'ℹ️' + }.get(issue.get('severity', 'info')) + + print(f"\n {severity_icon} {issue['issue']}") + print(f" Impact: {issue['impact']}") + if 'fix' in issue: + print(f" Fix: {issue['fix']}") + + # Generate fix script + self._generate_fix_script(issues) + + def _generate_fix_script(self, issues): + """Generate a script to fix all issues""" + fixes = [] + manual_fixes = [] + + for category_issues in issues.values(): + for issue in category_issues: + if 'fix' in issue and issue.get('severity') in ['high', 'medium']: + if issue.get('manual_only', False): + manual_fixes.append(issue['fix']) + else: + fixes.append(issue['fix']) + + if manual_fixes: + print("\n⚠️ Manual fixes required (cannot be automated):") + for fix in manual_fixes: + print(f" • {fix}") + print(" 💡 These require editing boot parameters or configuration files") + + +# Usage example and testing +if __name__ == "__main__": + import sys + + if len(sys.argv) < 2: + print("Usage: mesh_power_detective.py [options]") + print("\nOptions:") + print(" --monitor Monitor power events for 60 seconds") + print(" --fix Generate fix script automatically") + print("\nExamples:") + print(" sudo python3 mesh_power_detective.py wlan0") + print(" sudo python3 mesh_power_detective.py wlan0 --monitor") + print(" sudo python3 mesh_power_detective.py wlan0 --fix") + sys.exit(1) + + interface = sys.argv[1] + + # Check if interface exists + if not os.path.exists(f"/sys/class/net/{interface}"): + print(f"❌ Network interface '{interface}' not found") + print("💡 Try: ip link show") + sys.exit(1) + + detective = MeshPowerDetective(interface) + + print(f"🔋 WiFi Power Management Detective") + print(f"📡 Interface: {interface}") + print("=" * 50) + + try: + # Run all checks + detective.check_all_power_issues() + + # Optional monitoring + if "--monitor" in sys.argv: + detective.monitor_power_events(duration=60) + + if "--fix" in sys.argv: + print("\n💡 Fix script has been generated if issues were found") + + except KeyboardInterrupt: + print("\n👋 Analysis interrupted by user") + except Exception as e: + print(f"❌ Error: {e}") + print("💡 Make sure you're running with sudo privileges") diff --git a/MeshAnalyzer/files/mesh_roaming_detector.py b/MeshAnalyzer/files/mesh_roaming_detector.py new file mode 100644 index 0000000..038babe --- /dev/null +++ b/MeshAnalyzer/files/mesh_roaming_detector.py @@ -0,0 +1,435 @@ +#!/usr/bin/env python3 +""" +WiFi Mesh Roaming Detector +Detect and measure actual drops, reconnects, and roaming events +Save this file as: mesh_roaming_detector.py +Put it in the same folder as your main analyzer script +""" + +import subprocess +import time +import threading +from collections import deque +from datetime import datetime +import os + +class MeshRoamingDetector: + """Detect and measure actual drops, reconnects, and roaming events""" + + def __init__(self, interface): + self.interface = interface + self.events = deque(maxlen=1000) + self.current_bssid = None + self.monitoring = False + + def monitor_connection_state(self, interval=0.1): + """High-frequency monitoring to catch brief drops""" + self.monitoring = True + last_state = None + last_bssid = None + disconnect_start = None + + while self.monitoring: + # Get current connection state FAST + state = self._get_connection_state_fast() + + # Extract status and info from state + current_status = state.get('status', 'unknown') if isinstance(state, dict) else state + current_bssid = state.get('bssid') if isinstance(state, dict) else None + current_signal = state.get('signal', -100) if isinstance(state, dict) else -100 + + # Detect disconnection + if (isinstance(last_state, dict) and last_state.get('status') == "connected" and + current_status == "disconnected"): + disconnect_start = time.time() + self.events.append({ + 'type': 'disconnect', + 'timestamp': disconnect_start, + 'last_bssid': last_bssid, + 'last_signal': last_state.get('signal', -100) if isinstance(last_state, dict) else -100 + }) + + # Detect reconnection + elif (last_state == "disconnected" or + (isinstance(last_state, dict) and last_state.get('status') == "disconnected")) and current_status == "connected": + reconnect_time = time.time() + downtime = reconnect_time - disconnect_start if disconnect_start else 0 + + self.events.append({ + 'type': 'reconnect', + 'timestamp': reconnect_time, + 'downtime_seconds': downtime, + 'new_bssid': current_bssid, + 'new_signal': current_signal + }) + disconnect_start = None # Reset disconnect timer + + # Detect roaming (BSSID change without disconnect) + elif (current_status == "connected" and + (isinstance(last_state, dict) and last_state.get('status') == "connected") and + last_bssid and current_bssid and last_bssid != current_bssid): + + self.events.append({ + 'type': 'roam', + 'timestamp': time.time(), + 'from_bssid': last_bssid, + 'to_bssid': current_bssid, + 'from_signal': last_state.get('signal', -100) if isinstance(last_state, dict) else -100, + 'to_signal': current_signal, + 'seamless': True # No disconnect detected + }) + + # Update tracking variables + last_state = state + last_bssid = current_bssid + time.sleep(interval) + + def _get_connection_state_fast(self): + """Fastest possible connection state check with robust error handling""" + try: + # Use /proc/net/wireless for fastest reads + with open('/proc/net/wireless', 'r') as f: + lines = f.readlines() + for line in lines: + if self.interface in line: + # Parse signal level with error handling + parts = line.split() + if len(parts) >= 4: + try: + # Handle different possible formats + signal_str = parts[3].rstrip('.') + signal = int(float(signal_str)) + except (ValueError, IndexError): + signal = -100 + + # Get BSSID from iw (cached) + try: + cmd = f"iw dev {self.interface} link | grep 'Connected to'" + result = subprocess.run(cmd, shell=True, capture_output=True, + text=True, timeout=1) + + if "Connected to" in result.stdout: + bssid_part = result.stdout.split("Connected to ")[1].split()[0] + # Clean BSSID + bssid = bssid_part.split('(')[0].strip().upper() + + # Validate BSSID format + if len(bssid) == 17 and bssid.count(':') == 5: + return {'status': 'connected', 'signal': signal, 'bssid': bssid} + else: + return {'status': 'disconnected'} + else: + return {'status': 'disconnected'} + except (subprocess.TimeoutExpired, subprocess.SubprocessError): + return {'status': 'unknown'} + + return {'status': 'disconnected'} + + except (FileNotFoundError, PermissionError, OSError): + # Fallback method + try: + cmd = f"iw dev {self.interface} link" + result = subprocess.run(cmd, shell=True, capture_output=True, + text=True, timeout=2) + + if "Not connected" in result.stdout: + return {'status': 'disconnected'} + elif "Connected to" in result.stdout: + # Parse signal and BSSID with error handling + lines = result.stdout.split('\n') + bssid = None + signal = -100 + + for line in lines: + try: + if "Connected to" in line: + bssid_part = line.split("Connected to ")[1].split()[0] + bssid = bssid_part.split('(')[0].strip().upper() + # Validate BSSID format + if len(bssid) != 17 or bssid.count(':') != 5: + bssid = None + elif "signal:" in line: + signal_part = line.split("signal: ")[1].split()[0] + signal = int(float(signal_part)) + except (IndexError, ValueError): + continue + + if bssid: + return {'status': 'connected', 'signal': signal, 'bssid': bssid} + else: + return {'status': 'connected', 'signal': signal, 'bssid': 'unknown'} + else: + return {'status': 'unknown'} + + except (subprocess.TimeoutExpired, subprocess.SubprocessError, OSError): + return {'status': 'unknown'} + + def detect_microdropouts(self, duration=60): + """Detect drops shorter than 1 second""" + print(f"🔍 Monitoring for micro-dropouts for {duration} seconds...") + print("These are drops your system might not normally notice") + print("💡 Keep using your WiFi normally - browse, stream, etc.") + + # Clear previous events + self.events.clear() + + # Use rapid polling + monitor_thread = threading.Thread( + target=self.monitor_connection_state, + args=(0.05,) # 50ms polling + ) + monitor_thread.start() + + time.sleep(duration) + self.monitoring = False + monitor_thread.join() + + # Analyze micro-dropouts + dropouts = [e for e in self.events if e['type'] == 'reconnect' and e.get('downtime_seconds', 0) < 1.0] + + if dropouts: + print(f"\n🔴 Found {len(dropouts)} micro-dropouts:") + for d in dropouts: + print(f" • {d['downtime_seconds']:.3f}s dropout at {datetime.fromtimestamp(d['timestamp']).strftime('%H:%M:%S')}") + if d.get('new_bssid'): + print(f" Reconnected to: {d['new_bssid']} ({d.get('new_signal', 'unknown')}dBm)") + else: + print(f"\n✅ No micro-dropouts detected in {duration} seconds") + print("Your mesh is handling connections smoothly!") + + # Show any roaming events + roams = [e for e in self.events if e['type'] == 'roam'] + if roams: + print(f"\n🔄 Detected {len(roams)} seamless roaming events:") + for r in roams: + print(f" • {datetime.fromtimestamp(r['timestamp']).strftime('%H:%M:%S')}: {r['from_bssid']} → {r['to_bssid']}") + print(f" Signal: {r['from_signal']}dBm → {r['to_signal']}dBm") + + def measure_roaming_performance(self, walk_test=False): + """Measure actual roaming performance""" + print("📊 Measuring roaming performance...") + if walk_test: + print("🚶 Walk around your space now. Press Ctrl+C when done.") + print("💡 Try to move between different rooms/areas") + else: + print("🏠 Monitoring roaming events for 2 minutes...") + + # Clear previous events + self.events.clear() + + # Start monitoring + monitor_thread = threading.Thread( + target=self.monitor_connection_state, + args=(0.1,) # 100ms polling + ) + monitor_thread.start() + + try: + if walk_test: + # Let user control duration + input("Press Enter when you're done walking around...") + else: + time.sleep(120) # 2 minutes + except KeyboardInterrupt: + pass + + self.monitoring = False + monitor_thread.join() + + # Analyze roaming events + roams = [e for e in self.events if e['type'] == 'roam'] + disconnects = [e for e in self.events if e['type'] == 'disconnect'] + reconnects = [e for e in self.events if e['type'] == 'reconnect'] + + print(f"\n📊 Roaming Analysis:") + print(f" • Seamless roams: {len(roams)}") + print(f" • Disconnection events: {len(disconnects)}") + + if reconnects: + downtimes = [e['downtime_seconds'] for e in reconnects if e.get('downtime_seconds') is not None] + if downtimes: + print(f" • Average downtime: {sum(downtimes)/len(downtimes):.3f}s") + print(f" • Longest downtime: {max(downtimes):.3f}s") + print(f" • Shortest downtime: {min(downtimes):.3f}s") + + # Show roaming details + if roams: + print(f"\n🔄 Roaming Events:") + for r in roams: + time_str = datetime.fromtimestamp(r['timestamp']).strftime('%H:%M:%S') + signal_change = r['to_signal'] - r['from_signal'] + print(f" • {time_str}: {r['from_bssid'][:17]} → {r['to_bssid'][:17]}") + print(f" Signal change: {signal_change:+d}dBm ({r['from_signal']} → {r['to_signal']})") + + return { + 'seamless_roams': len(roams), + 'dropped_roams': len(disconnects), + 'avg_downtime': sum(e.get('downtime_seconds', 0) for e in reconnects) / len(reconnects) if reconnects else 0, + 'micro_dropouts': len([e for e in reconnects if e.get('downtime_seconds', 0) < 1.0]) + } + + def track_problem_transitions(self): + """Identify problematic BSSID transitions""" + transition_stats = {} + + # Analyze roaming events + for event in self.events: + if event['type'] == 'roam': + key = f"{event['from_bssid']} → {event['to_bssid']}" + if key not in transition_stats: + transition_stats[key] = {'count': 0, 'seamless': 0, 'avg_signal_change': []} + + transition_stats[key]['count'] += 1 + if event.get('seamless'): + transition_stats[key]['seamless'] += 1 + + signal_change = event['to_signal'] - event['from_signal'] + transition_stats[key]['avg_signal_change'].append(signal_change) + + if not transition_stats: + print("\n🔄 No roaming transitions detected") + print("💡 Try walking around or wait longer for roaming events") + return + + print("\n🔄 Transition Analysis:") + for transition, stats in transition_stats.items(): + success_rate = (stats['seamless'] / stats['count']) * 100 + avg_signal_change = sum(stats['avg_signal_change']) / len(stats['avg_signal_change']) + + status_emoji = "✅" if success_rate == 100 else "⚠️" if success_rate > 80 else "🔴" + print(f" {status_emoji} {transition}") + print(f" Success: {success_rate:.1f}% ({stats['seamless']}/{stats['count']} seamless)") + print(f" Avg signal change: {avg_signal_change:+.1f}dBm") + + def continuous_quality_monitor(self): + """Monitor connection quality during normal use""" + print("📊 Starting continuous connection quality monitor...") + print("This will track all drops and roaming events in the background") + print("💡 Use Ctrl+C to stop monitoring") + + # Create log file with proper error handling + try: + log_file = "/tmp/mesh_roaming_log.txt" + + with open(log_file, "a") as log: + log.write(f"\n--- Roaming Monitor Session Started {datetime.now()} ---\n") + log.flush() + + print(f"📝 Logging to: {log_file}") + print("🏃 Monitor running... use your WiFi normally") + + # Clear previous events + self.events.clear() + + # Start monitoring thread + monitor_thread = threading.Thread( + target=self.monitor_connection_state, + args=(0.1,) # 100ms polling + ) + monitor_thread.start() + + # Log events as they happen + last_event_count = 0 + while True: + time.sleep(1) # Check for new events every second + + if len(self.events) > last_event_count: + # New events to log + for event in list(self.events)[last_event_count:]: + timestamp = datetime.fromtimestamp(event['timestamp']).strftime('%H:%M:%S') + + if event['type'] == 'disconnect': + msg = f"{timestamp}: 🔴 CONNECTION LOST from {event.get('last_bssid', 'unknown')}" + log.write(msg + "\n") + print(msg) + + elif event['type'] == 'reconnect': + downtime = event.get('downtime_seconds', 0) + msg = f"{timestamp}: 🟢 RECONNECTED to {event.get('new_bssid', 'unknown')} (down {downtime:.3f}s)" + log.write(msg + "\n") + print(msg) + + elif event['type'] == 'roam': + signal_change = event['to_signal'] - event['from_signal'] + msg = f"{timestamp}: 🔄 ROAMED {event['from_bssid']} → {event['to_bssid']} ({signal_change:+d}dBm)" + log.write(msg + "\n") + print(msg) + + log.flush() + + last_event_count = len(self.events) + + except KeyboardInterrupt: + self.monitoring = False + monitor_thread.join() + print(f"\n👋 Monitoring stopped") + print(f"📊 Total events logged: {len(self.events)}") + print(f"📝 Log saved to: {log_file}") + + # Quick summary + roams = len([e for e in self.events if e['type'] == 'roam']) + drops = len([e for e in self.events if e['type'] == 'disconnect']) + if roams or drops: + print(f"📈 Summary: {roams} roams, {drops} drops") + else: + print("📈 Summary: Stable connection - no events detected") + + except (PermissionError, OSError) as e: + print(f"❌ Error accessing log file: {e}") + print("💡 Try running with sudo or check /tmp permissions") + + +# Usage example and testing +if __name__ == "__main__": + import sys + + if len(sys.argv) < 2: + print("Usage: mesh_roaming_detector.py [test_type]") + print("\nAvailable tests:") + print(" microdropouts - Detect brief connection drops (30 seconds)") + print(" roaming - Measure roaming performance (walk test)") + print(" transitions - Analyze problematic transitions") + print(" monitor - Continuous background monitoring") + print("\nExamples:") + print(" sudo python3 mesh_roaming_detector.py wlan0 microdropouts") + print(" sudo python3 mesh_roaming_detector.py wlan0 roaming") + print(" sudo python3 mesh_roaming_detector.py wlan0 monitor") + sys.exit(1) + + interface = sys.argv[1] + test_type = sys.argv[2] if len(sys.argv) > 2 else "microdropouts" + + # Check if interface exists + if not os.path.exists(f"/sys/class/net/{interface}"): + print(f"❌ Network interface '{interface}' not found") + print("💡 Try: ip link show") + sys.exit(1) + + detector = MeshRoamingDetector(interface) + + print(f"🔍 WiFi Mesh Roaming Detector") + print(f"📡 Interface: {interface}") + print(f"🧪 Test: {test_type}") + print("=" * 50) + + try: + if test_type == "microdropouts": + detector.detect_microdropouts(duration=30) + elif test_type == "roaming": + detector.measure_roaming_performance(walk_test=True) + elif test_type == "transitions": + # Run brief monitoring first to collect data + print("Collecting roaming data for 60 seconds...") + detector.measure_roaming_performance(walk_test=False) + detector.track_problem_transitions() + elif test_type == "monitor": + detector.continuous_quality_monitor() + else: + print(f"❌ Unknown test type: {test_type}") + print("Available: microdropouts, roaming, transitions, monitor") + + except KeyboardInterrupt: + print("\n👋 Test interrupted by user") + except Exception as e: + print(f"❌ Error: {e}") + print("💡 Make sure you're running with sudo privileges") diff --git a/MeshAnalyzer/files/mesh_venn_calculator.py b/MeshAnalyzer/files/mesh_venn_calculator.py new file mode 100644 index 0000000..0d96b99 --- /dev/null +++ b/MeshAnalyzer/files/mesh_venn_calculator.py @@ -0,0 +1,344 @@ +#!/usr/bin/env python3 +""" +Mesh Venn Diagram Calculator +Handles spatial overlap calculations for mesh nodes +""" + +import math +from typing import Dict, List, Tuple, Optional + +class MeshVennCalculator: + """Calculate mesh node spatial overlaps and positioning for Venn diagrams""" + + def __init__(self): + self.coverage_multiplier = 3.5 # Signal strength to coverage radius multiplier + self.min_radius = 40 # Minimum coverage radius in pixels + self.max_radius = 120 # Maximum coverage radius in pixels + + def calculate_coverage_radius(self, signal_dbm: int) -> int: + """Calculate coverage radius based on signal strength""" + # Convert signal strength to coverage radius + # Stronger signals = larger coverage areas + normalized_signal = max(0, signal_dbm + 100) # -100dBm becomes 0, -50dBm becomes 50 + radius = self.min_radius + (normalized_signal * self.coverage_multiplier) + return min(self.max_radius, max(self.min_radius, int(radius))) + + def create_smart_label(self, node_data: Dict, index: int, total_nodes: int, brand: str) -> str: + """Create contextually smart labels based on signal strength and position""" + signal = node_data['signal'] + + # Primary naming based on signal strength and logical position + if signal > -45: + base_name = "Main" + elif signal > -60: + base_name = "Near" if index == 1 else "Strong" + elif signal > -75: + base_name = "Mid" if total_nodes > 3 else "Extended" + else: + base_name = "Far" + + # Add distinguisher for multiple nodes in same category + if total_nodes > 4: + base_name += f"-{chr(65 + index)}" # A, B, C, etc. + elif total_nodes > 2 and signal <= -60: + # For 3-4 nodes, distinguish weaker ones + if base_name in ["Mid", "Extended", "Far"]: + base_name += f"-{index}" + + # Add brand context if available and not generic + if brand and brand not in ['unknown', 'generic']: + brand_short = brand.replace('_', ' ').replace('general', '').title() + # Clean up brand names + brand_map = { + 'Eero': 'eero', + 'Orbi Netgear': 'Orbi', + 'Google Nest': 'Nest', + 'Tp Link Deco': 'Deco', + 'Linksys Velop': 'Velop', + 'Asus': 'ASUS' + } + brand_clean = brand_map.get(brand_short, brand_short[:5]) + return f"{brand_clean} {base_name}" + + return base_name + + def generate_smart_labels(self, nodes_data: List[Dict], brand: str = 'unknown') -> List[str]: + """Generate meaningful labels for all nodes based on context""" + if not nodes_data: + return [] + + # Sort nodes by signal strength to assign logical roles + sorted_nodes = sorted(enumerate(nodes_data), key=lambda x: x[1]['signal'], reverse=True) + labels = [''] * len(nodes_data) + total_nodes = len(nodes_data) + + # Assign smart labels based on signal strength hierarchy + for rank, (orig_idx, node) in enumerate(sorted_nodes): + smart_label = self.create_smart_label(node, rank, total_nodes, brand) + labels[orig_idx] = smart_label + + return labels + + def calculate_optimal_positions(self, nodes_data: List[Dict]) -> List[Dict]: + """Calculate optimal positions for nodes to show realistic overlaps""" + node_count = len(nodes_data) + + if node_count == 1: + return [{'x': 50, 'y': 50}] + elif node_count == 2: + return self._position_two_nodes(nodes_data) + elif node_count == 3: + return self._position_three_nodes(nodes_data) + elif node_count == 4: + return self._position_four_nodes(nodes_data) + else: + return self._position_many_nodes(nodes_data) + + def _position_two_nodes(self, nodes_data: List[Dict]) -> List[Dict]: + """Position two nodes with appropriate overlap""" + # Sort by signal strength + sorted_nodes = sorted(nodes_data, key=lambda x: x['signal'], reverse=True) + + # Calculate radii + radius1 = self.calculate_coverage_radius(sorted_nodes[0]['signal']) + radius2 = self.calculate_coverage_radius(sorted_nodes[1]['signal']) + + # Position for 30-50% overlap + overlap_distance = (radius1 + radius2) * 0.6 # 40% overlap + + positions = [ + {'x': 40, 'y': 50}, + {'x': 40 + (overlap_distance / 4), 'y': 50} + ] + + return positions + + def _position_three_nodes(self, nodes_data: List[Dict]) -> List[Dict]: + """Position three nodes in triangle formation with overlaps""" + # Sort by signal strength + sorted_indices = sorted(range(len(nodes_data)), key=lambda i: nodes_data[i]['signal'], reverse=True) + + # Triangle positions with overlaps + base_positions = [ + {'x': 35, 'y': 35}, # Top-left + {'x': 65, 'y': 35}, # Top-right + {'x': 50, 'y': 65} # Bottom-center + ] + + # Adjust positions based on signal strengths for realistic overlaps + positions = [None] * 3 + for i, orig_idx in enumerate(sorted_indices): + positions[orig_idx] = base_positions[i] + + return positions + + def _position_four_nodes(self, nodes_data: List[Dict]) -> List[Dict]: + """Position four nodes in diamond/square formation""" + # Sort by signal strength + sorted_indices = sorted(range(len(nodes_data)), key=lambda i: nodes_data[i]['signal'], reverse=True) + + # Diamond positions for maximum overlaps + base_positions = [ + {'x': 40, 'y': 35}, # Top-left + {'x': 60, 'y': 35}, # Top-right + {'x': 35, 'y': 55}, # Bottom-left + {'x': 65, 'y': 55} # Bottom-right + ] + + positions = [None] * 4 + for i, orig_idx in enumerate(sorted_indices): + positions[orig_idx] = base_positions[i] + + return positions + + def _position_many_nodes(self, nodes_data: List[Dict]) -> List[Dict]: + """Position 5+ nodes in clustered spiral for overlaps""" + node_count = len(nodes_data) + positions = [] + + # Central cluster approach + center_x, center_y = 50, 50 + + for i in range(node_count): + if i == 0: + # Central node + positions.append({'x': center_x, 'y': center_y}) + else: + # Spiral outward + angle = (i - 1) * (2 * math.pi / (node_count - 1)) + radius_offset = 15 + ((i - 1) * 5) # Gradually increasing radius + + x = center_x + radius_offset * math.cos(angle) + y = center_y + radius_offset * math.sin(angle) + + # Keep within bounds + x = max(20, min(80, x)) + y = max(20, min(80, y)) + + positions.append({'x': x, 'y': y}) + + return positions + + def calculate_overlap_percentage(self, node1: Dict, node2: Dict) -> float: + """Calculate overlap percentage between two nodes""" + # Get positions and radii + x1, y1 = node1['position']['x'], node1['position']['y'] + x2, y2 = node2['position']['x'], node2['position']['y'] + r1 = self.calculate_coverage_radius(node1['signal']) + r2 = self.calculate_coverage_radius(node2['signal']) + + # Calculate distance between centers (scale from percentage to actual distance) + distance = math.sqrt((x2 - x1)**2 + (y2 - y1)**2) * 4 # Scale factor + + # Check if circles overlap + if distance >= r1 + r2: + return 0.0 # No overlap + + if distance <= abs(r1 - r2): + # One circle is completely inside the other + smaller_area = math.pi * min(r1, r2)**2 + larger_area = math.pi * max(r1, r2)**2 + return smaller_area / larger_area * 100 + + # Partial overlap calculation + # Using intersection area formula for two circles + try: + a = r1**2 * math.acos((distance**2 + r1**2 - r2**2) / (2 * distance * r1)) + b = r2**2 * math.acos((distance**2 + r2**2 - r1**2) / (2 * distance * r2)) + c = 0.5 * math.sqrt((-distance + r1 + r2) * (distance + r1 - r2) * (distance - r1 + r2) * (distance + r1 + r2)) + + intersection_area = a + b - c + total_area = math.pi * r1**2 + math.pi * r2**2 - intersection_area + + return (intersection_area / total_area) * 100 if total_area > 0 else 0.0 + except (ValueError, ZeroDivisionError): + # Fallback for edge cases + return 0.0 + + def generate_venn_data(self, nodes_data: List[Dict]) -> Dict: + """Generate complete Venn diagram data with smart labels, positions, radii, and overlaps""" + if not nodes_data: + return {'nodes': [], 'overlaps': [], 'total_coverage': 0} + + # Extract brand information if available + brand = 'unknown' + if nodes_data and 'brand' in nodes_data[0]: + brand = nodes_data[0]['brand'] + elif nodes_data and hasattr(nodes_data[0], 'brand'): + brand = nodes_data[0].brand + + # Generate smart labels for all nodes + smart_labels = self.generate_smart_labels(nodes_data, brand) + + # Calculate optimal positions + positions = self.calculate_optimal_positions(nodes_data) + + # Update nodes with smart labels, positions and radii + venn_nodes = [] + for i, node in enumerate(nodes_data): + radius = self.calculate_coverage_radius(node['signal']) + venn_node = { + **node, + 'label': smart_labels[i], # Use smart context-aware label + 'position': positions[i], + 'radius': radius, + 'coverage_area': math.pi * radius**2 + } + venn_nodes.append(venn_node) + + # Calculate all pairwise overlaps + overlaps = [] + for i in range(len(venn_nodes)): + for j in range(i + 1, len(venn_nodes)): + overlap_pct = self.calculate_overlap_percentage(venn_nodes[i], venn_nodes[j]) + if overlap_pct > 5: # Only record significant overlaps + overlaps.append({ + 'node1_id': i, + 'node2_id': j, + 'overlap_percentage': overlap_pct, + 'node1_label': venn_nodes[i]['label'], + 'node2_label': venn_nodes[j]['label'] + }) + + # Calculate total coverage area (accounting for overlaps) + total_coverage = sum(node['coverage_area'] for node in venn_nodes) + + return { + 'nodes': venn_nodes, + 'overlaps': overlaps, + 'total_coverage': total_coverage, + 'overlap_count': len(overlaps), + 'avg_overlap': sum(o['overlap_percentage'] for o in overlaps) / len(overlaps) if overlaps else 0 + } + + def get_overlap_quality_assessment(self, venn_data: Dict) -> Dict: + """Assess the quality of mesh overlap coverage""" + overlaps = venn_data['overlaps'] + nodes = venn_data['nodes'] + + if len(nodes) < 2: + return { + 'quality': 'single_node', + 'score': 100, + 'description': 'Single node - no overlap analysis needed' + } + + # Calculate overlap metrics + high_overlaps = [o for o in overlaps if o['overlap_percentage'] > 30] + medium_overlaps = [o for o in overlaps if 15 <= o['overlap_percentage'] <= 30] + low_overlaps = [o for o in overlaps if 5 <= o['overlap_percentage'] < 15] + + total_possible_overlaps = len(nodes) * (len(nodes) - 1) // 2 + actual_overlaps = len(overlaps) + + # Scoring + score = 0 + + # Reward good overlap coverage + if actual_overlaps / total_possible_overlaps > 0.7: + score += 30 + elif actual_overlaps / total_possible_overlaps > 0.5: + score += 20 + else: + score += 10 + + # Reward balanced overlaps + if high_overlaps and medium_overlaps: + score += 25 + elif medium_overlaps: + score += 15 + + # Reward having some overlaps + if actual_overlaps > 0: + score += 20 + + # Average overlap quality + avg_overlap = venn_data['avg_overlap'] + if avg_overlap > 25: + score += 25 + elif avg_overlap > 15: + score += 15 + elif avg_overlap > 5: + score += 10 + + # Determine quality level with smart descriptions + if score >= 80: + quality = 'excellent' + description = f'Excellent mesh overlap with optimal node placement - {actual_overlaps}/{total_possible_overlaps} connections provide seamless coverage' + elif score >= 60: + quality = 'good' + description = f'Good mesh overlap - {actual_overlaps}/{total_possible_overlaps} node pairs ensure reliable coverage' + elif score >= 40: + quality = 'fair' + description = f'Fair mesh overlap - some coverage gaps may exist between nodes' + else: + quality = 'poor' + description = f'Poor mesh overlap - significant coverage gaps likely, consider repositioning nodes' + + return { + 'quality': quality, + 'score': min(100, score), + 'description': description, + 'overlap_ratio': f'{actual_overlaps}/{total_possible_overlaps}', + 'avg_overlap_pct': round(avg_overlap, 1) + } diff --git a/MeshAnalyzer/images/mesh_screenshot-min.png b/MeshAnalyzer/images/mesh_screenshot-min.png new file mode 100644 index 0000000..c31615c Binary files /dev/null and b/MeshAnalyzer/images/mesh_screenshot-min.png differ diff --git a/MeshAnalyzer/images/mesh_screenshot-new.png b/MeshAnalyzer/images/mesh_screenshot-new.png new file mode 100644 index 0000000..c31615c Binary files /dev/null and b/MeshAnalyzer/images/mesh_screenshot-new.png differ diff --git a/MeshAnalyzer/images/readme b/MeshAnalyzer/images/readme new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/MeshAnalyzer/images/readme @@ -0,0 +1 @@ + diff --git a/MeshAnalyzer/readme.md b/MeshAnalyzer/readme.md new file mode 100644 index 0000000..e1d3ad8 --- /dev/null +++ b/MeshAnalyzer/readme.md @@ -0,0 +1,371 @@ +# 📡 WiFi Mesh Network Analyzer + +> **Linux-first mesh network diagnostics with roaming analysis, power management detection, and visual reporting** + +[![Python](https://img.shields.io/badge/Python-3.6+-blue.svg)](https://python.org) +[![NetworkManager](https://img.shields.io/badge/NetworkManager-Required-green.svg)]() +[![Zero Dependencies](https://img.shields.io/badge/Dependencies-Zero-brightgreen.svg)]() + +Real insight into how your mesh network actually behaves. See node connections, identify weak spots, track roaming performance, and diagnose power management issues - all without vendor lock-in or proprietary software. + +## ⚠️ Framework Support Disclaimer + +**Before implementing any power management changes recommended by this tool, please verify with Framework Support first.** + +While the WiFi Mesh Network Analyzer provides valuable diagnostic information and generates safe configuration scripts, power management settings should only be modified when addressing specific connectivity issues. The tool may recommend disabling PCIe ASPM (Active State Power Management) or NetworkManager power saving features, but these changes should only be applied if: + +- You are experiencing actual WiFi connectivity problems (disconnections, micro-dropouts, poor roaming) + +- The analysis clearly identifies power management as the root cause + +- Framework Support has reviewed your specific situation and confirmed the recommendation + +### Why This Matters +Power management features exist for good reasons - they extend battery life and reduce heat generation. Disabling them unnecessarily can impact your system's efficiency without providing any benefits. The diagnostic tools help identify potential power management conflicts, but not every detection requires action. + +### Recommended Workflow + +- Run the analysis to identify potential issues: Run the script per the instructions. + +- Document your specific symptoms (connection drops, poor performance, etc.) + +- Contact Framework Support with both your symptoms and the tool's findings +- Apply recommended changes only after confirmation from Support +- Test thoroughly and revert changes if they don't resolve your specific issues + +### Contact Framework Support + +[Contact](https://framework.kustomer.help/contact/support-request-ryon9uAuq) - Ask to send your findings to the Linux Support Team + +Remember: _These diagnostic tools are designed to help identify issues, not automatically fix them. Always verify recommendations with Framework Support before making system changes._ + +## 📚 Table of Contents + +- [🚀 Key Features](#-key-features) +- [🎯 Why This Tool?](#-why-this-tool) +- [📋 Quick Start](#-quick-start) +- [🎛️ Main Features](#️-main-features) +- [📊 What You Get](#-what-you-get) +- [🔧 Advanced Usage](#-advanced-usage) +- [🛡️ Compatibility](#️-compatibility) +- [🔍 Troubleshooting](#-troubleshooting) +- [📱 Example Output](#-example-output) +- [💡 Pro Tips](#-pro-tips) +- [🔗 Related Tools](#-related-tools) + +## 🚀 Key Features + +### 🔍 **Mesh Intelligence** +- **Topology Mapping** - Visual mesh node detection and relationship analysis +- **Brand Recognition** - 500+ OUI database covering Eero, Orbi, Google Nest, Ubiquiti, enterprise systems +- **Coverage Analysis** - Spatial zone mapping with signal strength distribution +- **Overlap Detection** - Venn diagram analysis of node coverage areas + +### 🔄 **Roaming Analysis** +- **Micro-dropout Detection** - Catch 50ms connection drops your system misses +- **Handoff Quality Testing** - Measure real roaming performance while walking around +- **Transition Monitoring** - Real-time tracking of problematic node switches +- **Pattern Recognition** - Identify roaming loops and sticky client issues + +### 🔋 **Power Management** +- **WiFi Power Saving Detection** - Find power management causing disconnects +- **Driver-Specific Analysis** - Intel, MediaTek, Qualcomm, Atheros optimization +- **USB Autosuspend Checking** - Detect USB WiFi adapter suspension problems +- **Automated Fix Generation** - Create executable scripts to resolve power issues + +### 📊 **Professional Reporting** +- **Interactive HTML Reports** - Modern dark theme with glassmorphism design +- **Real-time Visualizations** - Hover effects, click-to-copy BSSIDs, smooth animations +- **Mobile-Responsive** - Optimized for all devices with responsive layout +- **Historical Tracking** - Performance trends and stability scoring over time + +## 🎯 Why This Tool? + +### **Mesh Networks Are Opaque** +Commercial mesh systems hide diagnostics behind limited apps or cloud dashboards. When performance drops or devices won't roam correctly, you're left guessing. This tool surfaces that data directly from the network. + +### **Common Problems It Solves** +- Devices sticking to weak access points instead of roaming +- Random disconnections and connection drops +- Coverage dead zones or excessive overlap +- Power management issues causing false disconnects +- Micro-dropouts during streaming or gaming +- Poor handoff performance between nodes + +### **Linux-First Approach** +No vendor lock-in, no cloud dependencies, no proprietary software. Uses standard Linux WiFi tools with intelligent analysis on top. + +## 📋 Quick Start + +### Prerequisites +``` +# Ubuntu/Debian +sudo apt update && sudo apt install iw + +# Fedora/RHEL +sudo dnf install iw + +# Arch Linux (NetworkManager required - not iwd compatible) +sudo pacman -S iw +``` + +### Installation +``` +mkdir mesh_analyzer && cd mesh_analyzer +``` + +``` +wget https://raw.githubusercontent.com/FrameworkComputer/linux-docs/main/MeshAnalyzer/files/mesh_analyzer.py && \ +wget https://raw.githubusercontent.com/FrameworkComputer/linux-docs/main/MeshAnalyzer/files/mesh_html_reporter.py && \ +wget https://raw.githubusercontent.com/FrameworkComputer/linux-docs/main/MeshAnalyzer/files/mesh_venn_calculator.py && \ +wget https://raw.githubusercontent.com/FrameworkComputer/linux-docs/main/MeshAnalyzer/files/mesh_roaming_detector.py && \ +wget https://raw.githubusercontent.com/FrameworkComputer/linux-docs/main/MeshAnalyzer/files/mesh_power_detective.py +``` + +(Only needed if you would rather run with sudo ./ instead of sudo python3) +``` +chmod +x *.py +``` + +### Basic Analysis +``` +sudo python3 mesh_analyzer.py +``` + +### Complete Analysis (Recommended) (run both lines below to include an archive for support) +``` +sudo python3 mesh_analyzer.py --check-power --detect-dropouts --roaming-test --html-report +``` +then +``` +sudo python3 mesh_analyzer.py --create-archive +``` + +## 🎛️ Main Features + +| Feature | Command | Description | +|---------|---------|-------------| +| **Basic Analysis** | `sudo python3 mesh_analyzer.py` | Topology mapping and signal analysis | +| **HTML Report** | `--html-report` | Interactive visual report with charts | +| **Roaming Test** | `--roaming-test` | Walk-around handoff quality testing | +| **Micro-dropouts** | `--detect-dropouts` | 30-second connection stability test | +| **Power Check** | `--check-power` | WiFi power management issue detection | +| **Continuous Monitor** | `--monitor` | Real-time monitoring every 60 seconds | +| **Archive Creation** | `--create-archive` | Compressed analysis logs | + +## 📊 What You Get + +### **Terminal Output** +- Comprehensive mesh topology with node relationships +- Signal strength zone mapping (Primary/Secondary/Tertiary/Fringe) +- Historical performance tracking with stability scores +- Venn overlap analysis with quality scoring +- Roaming quality assessment and micro-dropout detection +- Power management issue identification with automated fixes +- Intelligent optimization recommendations + +### **HTML Report** +- Professional dark theme with modern glassmorphism design +- Interactive mesh topology visualization with hover effects +- Advanced signal strength distribution charts +- Visual Venn overlap diagrams with SVG rendering +- Comprehensive coverage analysis with spatial zones +- Performance trends and historical data tracking +- Click-to-copy BSSID functionality +- Mobile-responsive design optimized for all devices + +### **Example Analysis Results** +``` +🏷️ Brand: Eero +🔧 Type: Tri-Band Mesh +🏠 Topology: 4 nodes, 8 radios +📶 Mesh Quality: Good Topology (Score: 70/100) +🔄 Coverage Overlap: Excellent (Score: 100/100) +🔋 Power Issues: 1 found (PCIe ASPM) +📈 Current BSSID Stability: 100/100 (Excellent) +``` + +## 🔧 Advanced Usage + +### **Roaming Analysis** +``` +# Detect micro-dropouts (30 seconds) with visual report +sudo python3 mesh_analyzer.py --html-report --detect-dropouts +``` +``` +# Test roaming quality while walking with comprehensive reporting +sudo python3 mesh_analyzer.py --html-report --roaming-test +``` +``` +# Continuous roaming monitoring with real-time HTML updates +sudo python3 mesh_analyzer.py --html-report --monitor-roaming +``` + +### **Power Management** +``` +# Check for power issues +sudo python3 mesh_analyzer.py --html-report --check-power +``` + +### **Monitoring & Logging** +``` +# Continuous monitoring (60s intervals) +sudo python3 mesh_analyzer.py --monitor +``` +``` +# Custom scan interval (2 minutes) +sudo python3 mesh_analyzer.py --monitor --scan-interval 120 +``` +``` +# Show storage information +sudo python3 mesh_analyzer.py --storage-info +``` + +### **Data Management** +``` +# Reset corrupted history files +sudo python3 mesh_analyzer.py --reset-history +``` +``` +# Create archive without new analysis +sudo python3 mesh_analyzer.py --archive-only +``` + +## 🛡️ Compatibility + +### **Supported Systems** +- **Linux Distributions**: Ubuntu, Debian, Fedora, Arch, openSUSE, Pop!_OS, Mint +- **Network Managers**: NetworkManager (iwd support not yet implemented) +- **WiFi Hardware**: Intel, MediaTek, Qualcomm, Broadcom, Atheros, Realtek +- **Mesh Systems**: Eero, Orbi, Google Nest, ASUS, TP-Link, Linksys, Ubiquiti, enterprise systems + +### **Requirements** +- Python 3.6+ (standard on most Linux systems) +- NetworkManager (not compatible with iwd) +- Root/sudo access for WiFi scanning +- Active mesh network connection for best results + +### **Zero Dependencies** +Uses only Python standard library - no pip installs required! + +## 🔍 Troubleshooting + +### **Common Issues** + +**"No WiFi interface found"** +``` +nmcli device status # Check available interfaces +ip link show # List all network interfaces +``` + +**"0 access points found"** +- Ensure you have sudo privileges for WiFi scanning +- Try moving closer to mesh nodes +- Some enterprise networks restrict scanning + +**"Permission denied"** +- Must run with `sudo` for WiFi scanning capabilities +- Files are automatically created with correct user permissions + +**Module import errors** +- Ensure all 5 Python files are in the same directory +- Check file permissions with `ls -la *.py` +- Optional modules will gracefully degrade if missing + +### **Why Some Nodes Don't Appear** + +Not all mesh nodes will always show up in scans. This is normal due to: + +- **Band Steering** - Mesh systems hide certain bands or backhaul links +- **Scan Timing** - Nodes may broadcast intermittently or reduce beaconing when idle +- **Power Management** - Nodes in power-saving modes reduce visibility +- **Driver Limitations** - Some WiFi chipsets don't report all frequencies reliably +- **DFS Channels** - Regulatory restrictions on 5GHz channels +- **Distance/Interference** - Remote nodes may be too weak to detect + +**Solutions**: Run multiple scans, use roaming analysis features, or reposition your device. + +## 📱 Example Output + +### **Terminal Analysis** +``` +🧠 WiFi Mesh Network Analyzer +============================================================ +📡 Interface: wlp5s0 +🔗 Connected: Slower | D8:8E:D4:7D:2E:C8 | 7015 MHz | -53 dBm + +🏷️ Brand: Eero +🔧 Type: Tri-Band Mesh +🏠 Topology: 4 nodes, 8 radios +📶 Mesh Topology: 🟢 Good Topology (Quality Score: 70/100) + +🗺️ SPATIAL COVERAGE ANALYSIS: + 🟢 Primary Zone: 2 nodes (-44 to -33dBm) + 🟠 Tertiary Zone: 1 nodes (-70 to -70dBm) + 🔴 Fringe Zone: 1 nodes (-89 to -89dBm) + +🔄 VENN OVERLAP ANALYSIS: + 🟢 Coverage Overlap Quality: Excellent (Score: 100/100) + 📊 Excellent mesh overlap - 6/6 node pairs overlapping + +📈 Current BSSID Performance Analysis: + 🟢 Stability Score: 100.0/100 (Excellent) + ✅ Success Rate: 100.0% + +✅ No micro-dropouts detected in 30 seconds +🔋 Power Issues: 1 found - PCIe ASPM configuration +``` + +### **HTML Report Preview** +![Mesh Analysis Report](https://raw.githubusercontent.com/FrameworkComputer/linux-docs/refs/heads/main/MeshAnalyzer/images/mesh_screenshot-new.png) + +## 💡 Pro Tips + +### **For Best Results** +- Connect to the mesh network you want to analyze before running +- Run multiple analyses over time for better historical data +- Use `--roaming-test` while walking around different areas +- Check `--detect-dropouts` if experiencing connection issues + +### **Performance Optimization** +- Large mesh networks may take 15-30 seconds to scan completely +- Use `--monitor` for long-term network health tracking +- HTML reports work best in modern browsers with JavaScript enabled +- All analysis data is saved locally for privacy + +### **Troubleshooting Workflow** +1. Start with basic analysis to identify topology +2. Use `--check-power` if experiencing frequent disconnects +3. Run `--detect-dropouts` to catch micro-interruptions +4. Use `--roaming-test` while moving between coverage areas +5. Generate `--html-report` for comprehensive visual analysis + +### **Safety Notes** +- Always review generated power management fix scripts before execution +- Enterprise networks may block some WiFi scanning capabilities +- Tool respects Linux-first workflows and privacy (no cloud dependencies) + +## 🔗 Related Tools + +### **General WiFi Diagnostics** +- **[Enhanced WiFi Analyzer](https://github.com/FrameworkComputer/linux-docs/tree/main/Enhanced-WiFi-Analyzer#-enhanced-wifi-analyzer)** - Comprehensive WiFi diagnostics with DFS monitoring, VPN integration, and modern chipset support + +### **When to Use Which Tool** +| Scenario | WiFi Mesh Analyzer | Enhanced WiFi Analyzer | +|----------|-------------------|------------------------| +| **Mesh network optimization** | ✅ Specialized analysis | ⚪ Basic detection | +| **Node topology mapping** | ✅ Visual mesh analysis | ⚪ Not covered | +| **Roaming performance** | ✅ Detailed testing | ⚪ Limited coverage | +| **General WiFi issues** | ⚪ Basic coverage | ✅ Comprehensive | +| **DFS disconnections** | ⚪ Not specialized | ✅ Expert analysis | +| **VPN conflicts** | ⚪ Not covered | ✅ Modern VPN support | +| **Chipset optimization** | ⚪ Limited | ✅ Advanced detection | + +### **Complementary Workflow** +1. **WiFi issues first** - Use Enhanced WiFi Analyzer for connectivity problems, DFS issues, VPN conflicts +2. **Mesh optimization** - Use WiFi Mesh Analyzer for topology analysis and roaming performance +3. **Best coverage** - Both tools together provide complete WiFi environment understanding + +--- + +**🔧 Production-ready software actively seeking feedback! Please test with different mesh systems, Linux distributions, and network environments.** diff --git a/Network-Diagnostic-Scripts/5ghz-diag.sh b/Network-Diagnostic-Scripts/5ghz-diag.sh new file mode 100644 index 0000000..b5545a2 --- /dev/null +++ b/Network-Diagnostic-Scripts/5ghz-diag.sh @@ -0,0 +1,636 @@ +#!/bin/bash + +# Enhanced WiFi 5GHz Diagnostic Script +# Compatible with immutable distributions (Bazzite, Project Bluefin) + +# Function to check if reboot is needed +check_reboot_required() { + if [ -f /var/run/reboot-required ]; then + return 0 + elif [ "$IS_IMMUTABLE" -eq 1 ] && rpm-ostree status | grep -q "pending deployment"; then + return 0 + elif [ -f /usr/bin/needs-restarting ]; then + needs-restarting -r >/dev/null 2>&1 + if [ $? -eq 1 ]; then + return 0 + fi + fi + return 1 +} + +# Function to check if we have sudo privileges +check_sudo() { + if [ "$EUID" -eq 0 ]; then + return 0 + else + if sudo -n true 2>/dev/null; then + return 0 + else + echo "Note: Some features require sudo privileges for full functionality" + echo "Consider running: sudo $0" + return 1 + fi + fi +} + +# Function to run command with proper privileges +run_with_privilege() { + if [ "$EUID" -eq 0 ]; then + "$@" + else + if sudo -n true 2>/dev/null; then + sudo "$@" + else + # Fallback without sudo + "$@" 2>/dev/null + fi + fi +} + +# Function to install missing dependencies +install_dependencies() { + echo "Checking for missing dependencies..." + MISSING_DEPS=() + + # Check required tools + for cmd in iw nmcli ip; do + if ! command -v $cmd >/dev/null 2>&1; then + MISSING_DEPS+=($cmd) + fi + done + + if [ ${#MISSING_DEPS[@]} -eq 0 ]; then + echo "All dependencies are installed." + return 0 + fi + + echo "Missing dependencies: ${MISSING_DEPS[@]}" + read -p "Would you like to install them? (y/N): " install_confirm + + if [[ "$install_confirm" =~ ^[Yy]$ ]]; then + if [ "$IS_IMMUTABLE" -eq 1 ]; then + echo "Installing on immutable system..." + for dep in "${MISSING_DEPS[@]}"; do + case $dep in + "iw") + sudo rpm-ostree install iw + ;; + "nmcli") + sudo rpm-ostree install NetworkManager + ;; + "ip") + sudo rpm-ostree install iproute + ;; + esac + done + + echo "Installation complete. A reboot is required for changes to take effect." + return 1 + else + # Traditional package installation + if command -v apt >/dev/null 2>&1; then + sudo apt update + sudo apt install -y iw network-manager iproute2 + elif command -v dnf >/dev/null 2>&1; then + sudo dnf install -y iw NetworkManager iproute + elif command -v pacman >/dev/null 2>&1; then + sudo pacman -Sy --noconfirm iw networkmanager iproute2 + fi + fi + else + echo "Skipping dependency installation. Some features may not work." + return 0 + fi +} + +# Main script +( + echo "=== WiFi 5GHz Comprehensive Diagnostic Report ===" + echo "Generated on: $(date)" + echo "" + + # Check sudo status + check_sudo + HAS_SUDO=$? + + # Detect if running on an immutable distribution + IS_IMMUTABLE=0 + if [ -f /usr/lib/os-release ]; then + if grep -qiE "bazzite|bluefin|silverblue|kinoite" /usr/lib/os-release; then + IS_IMMUTABLE=1 + echo "Detected immutable distribution" + fi + fi + echo "" + + # Check and install dependencies + install_dependencies + DEPS_INSTALLED=$? + + # Check if reboot is required + if check_reboot_required || [ $DEPS_INSTALLED -eq 1 ]; then + echo "" + echo "=== REBOOT REQUIRED ===" + echo "A system reboot is required to apply changes." + echo "Please reboot your system and run this script again." + echo "" + read -p "Would you like to reboot now? (y/N): " reboot_confirm + if [[ "$reboot_confirm" =~ ^[Yy]$ ]]; then + echo "Rebooting system..." + sudo reboot + else + echo "Please reboot manually when convenient and run this script again." + exit 0 + fi + fi + + # Regulatory Domain Information + echo "=== Regulatory Domain Information ===" + iw reg get + echo "" + + # Interface Detection + echo "=== Interface Detection ===" + IFACE=$(ip route get 1.1.1.1 2>/dev/null | grep dev | awk '{print $5}') + echo "Detected Interface: ${IFACE:-Unknown}" + echo "Interface Status:" + ip link show $IFACE 2>/dev/null + echo "" + + # Hardware Capabilities + echo "=== Hardware Capabilities ===" + if [ -n "$IFACE" ]; then + PHY=$(iw dev $IFACE info | grep wiphy | awk '{print $2}') + echo "Wireless Card Information:" + lspci -vv -s $(lspci | grep -i network | awk '{print $1}') | grep -E "Network|Subsystem|Kernel driver" + echo "" + echo "Supported Bands and Frequencies:" + iw phy$PHY info | awk ' + /Band [1-4]:/ { band=$0; print band; show=1; next } + /Band/ && !/Band [1-4]:/ { show=0 } + show && /Frequencies:|Bitrates:|HT|VHT|HE/ { print $0 } + show && /\* [0-9]+ MHz/ { print $0 } + ' + echo "" + echo "5GHz Band Capabilities:" + iw phy$PHY info | sed -n '/Band 2:/,/Band [34]/p' | grep -v "Band [34]" + echo "" + echo "Active 5GHz Features:" + iw phy$PHY info | grep -E "HT40|VHT|HE|160MHz|DFS" + fi + echo "" + + # Current Connection Status + echo "=== Current Connection Status ===" + if [ -n "$IFACE" ]; then + iwconfig $IFACE 2>/dev/null + echo "" + echo "Detailed Link Information:" + iw dev $IFACE link + echo "" + echo "Station Info (if connected):" + iw dev $IFACE station dump + fi + echo "" + + # 5GHz Network Scan + echo "=== Scanning for 5GHz Networks ===" + if [ -n "$IFACE" ]; then + echo "5GHz Networks Found:" + + if [ "$HAS_SUDO" -eq 0 ]; then + run_with_privilege iw dev $IFACE scan | awk ' + BEGIN { print_in_progress=0 } + /BSS/ { + if (print_in_progress && freq >= 5000) { + printf " Network: %s\n", ssid + printf " Frequency: %s MHz (Channel %s)\n", freq, channel + printf " Signal: %s dBm\n", signal + if (security != "") { + printf " Security:%s\n", security + } + printf " BSSID: %s\n\n", bss + } + bss=$2 + signal="" + security="" + freq="" + ssid="" + channel="" + print_in_progress=0 + } + /freq:/ { freq=$2 } + /signal:/ { signal=$2 } + /SSID:/ { + gsub(/^[ \t]*SSID: /, "") + ssid=$0 + print_in_progress=1 + } + /primary channel:/ { channel=$3 } + /WPA|RSN/ { security=security " " $0 } + END { + if (print_in_progress && freq >= 5000) { + printf " Network: %s\n", ssid + printf " Frequency: %s MHz (Channel %s)\n", freq, channel + printf " Signal: %s dBm\n", signal + if (security != "") { + printf " Security:%s\n", security + } + printf " BSSID: %s\n\n", bss + } + } + ' + else + echo "Note: Scanning requires sudo privileges" + echo "For best results, run with: sudo $0" + fi + else + echo " Interface not detected, cannot scan." + fi + echo "" + + # Channel Availability + echo "=== 5GHz Channel Availability ===" + if [ -n "$IFACE" ]; then + iw phy$(iw dev $IFACE info | grep wiphy | awk '{print $2}') channels | grep -E "5[0-9]{3} MHz" + fi + echo "" + + # NetworkManager Configuration + echo "=== NetworkManager Configuration ===" + nmcli radio wifi + nmcli device show $IFACE | grep -E "WIFI-PROPERTIES|GENERAL|CAPABILITIES" + echo "" + echo "Current Connection Profile (if connected):" + CURRENT_CONNECTION=$(nmcli device show $IFACE | grep GENERAL.CONNECTION | awk '{print $2}') + if [ "$CURRENT_CONNECTION" != "--" ]; then + nmcli connection show "$CURRENT_CONNECTION" | grep -E "802-11-wireless|ipv4|ipv6|frequency" + fi + echo "" + + # Firmware Information + echo "=== Firmware Information ===" + echo "Distribution Type: $(if [ "$IS_IMMUTABLE" -eq 1 ]; then echo "Immutable"; else echo "Traditional"; fi)" + echo "" + echo "Linux Firmware Package Version:" + if [ "$IS_IMMUTABLE" -eq 1 ]; then + # For immutable distributions - get linux-firmware version directly + if command -v rpm-ostree >/dev/null 2>&1; then + # Get version from rpm database + FIRMWARE_VERSION=$(rpm -q --queryformat '%{VERSION}-%{RELEASE}' linux-firmware 2>/dev/null || true) + if [ -n "$FIRMWARE_VERSION" ]; then + echo " linux-firmware: $FIRMWARE_VERSION" + else + # Fallback to rpm-ostree status + rpm-ostree status | grep -A5 "^ \* " | grep -E "linux-firmware|Version" | head -2 || echo " Not found" + fi + fi + echo "" + echo "Using layered packages:" + rpm-ostree status | grep -A1 "^ \* " | grep LayeredPackages || echo " None" + else + # For traditional distributions + if command -v dpkg >/dev/null 2>&1; then + dpkg -l | grep linux-firmware | awk '{print $2 " version: " $3}' + elif command -v pacman >/dev/null 2>&1; then + pacman -Q linux-firmware 2>/dev/null + elif command -v rpm >/dev/null 2>&1; then + rpm -q --queryformat 'linux-firmware version: %{VERSION}-%{RELEASE}\n' linux-firmware 2>/dev/null || echo " Not found" + else + echo " Package manager not detected" + fi + fi + echo "" + echo "Wireless Firmware Files:" + find /lib/firmware /usr/lib/firmware -path "*/mediatek/*MT7922*" -o -path "*/mediatek/*mt7922*" -o -path "*/mt*" -type f 2>/dev/null | while read file; do + if [ -f "$file" ]; then + stat -c "%n: %y" "$file" + fi + done | head -10 + echo "" + echo "Loaded Firmware Version:" + if [ -n "$IFACE" ]; then + # Use dmesg with proper privileges + run_with_privilege dmesg | grep -iE "firmware|iwlwifi|mt79" | grep -v "Direct firmware load" | tail -5 + fi + echo "" + + # Driver and Module Information + echo "=== Driver and Module Information ===" + lspci -nnk 2>/dev/null | grep -A 3 -i network + echo "" + if [ -n "$IFACE" ]; then + MODULE=$(ls -l /sys/class/net/$IFACE/device/driver/module 2>/dev/null | awk -F/ '{print $NF}') + if [ -n "$MODULE" ]; then + echo "Wireless Module: $MODULE" + echo "Module Parameters:" + for param in /sys/module/$MODULE/parameters/*; do + if [ -f "$param" ]; then + echo " $(basename $param): $(cat $param 2>/dev/null)" + fi + done + echo "" + echo "Module Information:" + modinfo $MODULE 2>/dev/null | grep -E "filename|version|firmware|description" + fi + fi + echo "" + + # Connection Event Analysis + echo "=== Connection Event Analysis (Last 24 Hours) ===" + echo "Connection Attempts, Successes, and Failures:" + journalctl -u NetworkManager -u wpa_supplicant --since "24 hours ago" --no-pager 2>/dev/null | \ + grep -Ei 'attempting|associating|authenticating|connected to|successfully|failed|fail|disconnect|error|auth_timeout|deauthenticating|reason|5[0-9]{3}|band|freq|channel' | \ + awk '{print $1,$2,$3,$0}' | tail -50 + echo "" + + # Error and Warning Analysis + echo "=== Recent Errors and Warnings ===" + journalctl -u NetworkManager -u wpa_supplicant --since "24 hours ago" --no-pager 2>/dev/null | \ + grep -i -E 'error|warning|fail|timeout' | tail -10 + echo "" + + # Power Management Settings + echo "=== Power Management Settings ===" + if [ -n "$IFACE" ]; then + echo "Power Saving Status:" + iw dev $IFACE get power_save + echo "" + echo "TLP/Power Management Configuration:" + if [ -f /etc/tlp.conf ]; then + grep -E "WIFI|POWER" /etc/tlp.conf 2>/dev/null | grep -v ^# + else + echo " TLP not installed" + fi + fi + echo "" + + # Band Steering and Roaming Settings + echo "=== Band Steering and Roaming Settings ===" + if [ -n "$IFACE" ]; then + echo "BSS Transition Management Capability:" + iw phy$(iw dev $IFACE info | grep wiphy | awk '{print $2}') info | grep -A 5 "Supported extended features:" | grep -E "BSS|FT|FILS" + echo "" + echo "Current Roaming Behavior:" + ip addr show $IFACE 2>/dev/null | grep -E "brd|scope|valid_lft" + echo "" + echo "NetworkManager WiFi Backend Configuration:" + if [ -f /etc/NetworkManager/conf.d/wifi_backend.conf ]; then + cat /etc/NetworkManager/conf.d/wifi_backend.conf + else + echo " No custom WiFi backend configuration found" + fi + echo "" + echo "WiFi Band Selection Configuration:" + grep -r "wifi.band-" /etc/NetworkManager/ 2>/dev/null || echo " No band selection config found" + fi + echo "" + + # 5GHz Connection Issues + echo "=== 5GHz Connection Issues ===" + if [ -n "$IFACE" ]; then + echo "Checking for 5GHz connection problems:" + + # Check if hardware supports 5GHz + if iw phy$(iw dev $IFACE info | grep wiphy | awk '{print $2}') info | grep -qE "5[0-9][0-9][0-9].*MHz"; then + echo "✓ Hardware supports 5GHz" + else + echo "✗ Hardware does not support 5GHz" + fi + + # Check if connected to 5GHz or 6GHz + CURRENT_FREQ=$(iw dev $IFACE link 2>/dev/null | grep freq | awk '{print $2}') + if [ -n "$CURRENT_FREQ" ]; then + FREQ_NUM=$(echo "$CURRENT_FREQ" | cut -d. -f1) + if [ "$FREQ_NUM" -ge 5925 ]; then + echo "✓ Connected to 6GHz/WiFi 6E ($CURRENT_FREQ MHz)" + elif [ "$FREQ_NUM" -ge 5000 ]; then + echo "✓ Connected to 5GHz ($CURRENT_FREQ MHz)" + else + echo "✗ Connected to 2.4GHz ($CURRENT_FREQ MHz)" + + # Check if 5GHz of the same SSID is available + CURRENT_SSID=$(iw dev $IFACE link 2>/dev/null | grep SSID | awk '{$1=""; print $0}' | sed 's/^ *//') + if [ -n "$CURRENT_SSID" ] && [ "$HAS_SUDO" -eq 0 ]; then + if run_with_privilege iw dev $IFACE scan 2>/dev/null | grep -A5 "$CURRENT_SSID" | grep -q "freq: 5[0-9][0-9][0-9]"; then + echo " 5GHz version of '$CURRENT_SSID' is available" + echo " Band steering might be failing or disabled" + fi + fi + fi + else + echo "! Not connected to any network" + fi + + # Check band selection settings + echo "" + echo "Band Selection Settings:" + if nmcli connection show "$CURRENT_SSID" 2>/dev/null | grep -q "wifi.band"; then + nmcli connection show "$CURRENT_SSID" | grep wifi.band + else + echo " No specific band preference set" + fi + + # Check for DFS issues + echo "" + echo "DFS Channel Issues:" + if journalctl -u NetworkManager --since "24 hours ago" 2>/dev/null | grep -qi "dfs\|radar"; then + echo " DFS/Radar events detected in logs" + else + echo " No DFS issues detected" + fi + + # Check driver/firmware issues + echo "" + echo "Driver/Firmware Issues:" + if run_with_privilege dmesg | grep -i mt7921 | grep -qi "error\|fail\|timeout"; then + echo " Driver/firmware errors detected" + run_with_privilege dmesg | grep -i mt7921 | grep -i "error\|fail\|timeout" | tail -3 + else + echo " No driver errors detected" + fi + fi + echo "" + + # Summary and Recommendations + echo "=== Summary and Recommendations ===" + echo "" + + # Create formatted summary box + echo "+----------------------------------------+" + echo "| WiFi DIAGNOSTIC SUMMARY |" + echo "+----------------------------------------+" + echo "" + + # Connection status with color coding + if [ -n "$IFACE" ]; then + CURRENT_SSID=$(iw dev $IFACE link 2>/dev/null | grep SSID | awk '{$1=""; print $0}' | sed 's/^ *//') + CURRENT_FREQ=$(iw dev $IFACE link 2>/dev/null | grep freq | awk '{print $2}') + + echo "🛜 CONNECTION STATUS" + echo "-------------------" + if [ -n "$CURRENT_FREQ" ]; then + FREQ_NUM=$(echo "$CURRENT_FREQ" | cut -d. -f1) + if [ "$FREQ_NUM" -ge 5925 ]; then + echo " ✅ Connected to: $CURRENT_SSID" + echo " ✅ Band: WiFi 6E (6GHz - $CURRENT_FREQ MHz)" + CONNECTION_STATUS="EXCELLENT" + BAND_TYPE="6GHz" + elif [ "$FREQ_NUM" -ge 5000 ]; then + echo " ✅ Connected to: $CURRENT_SSID" + echo " ✅ Band: 5GHz ($CURRENT_FREQ MHz)" + CONNECTION_STATUS="GOOD" + BAND_TYPE="5GHz" + else + echo " ⚠️ Connected to: $CURRENT_SSID" + echo " ⚠️ Band: 2.4GHz ($CURRENT_FREQ MHz)" + CONNECTION_STATUS="SUBOPTIMAL" + BAND_TYPE="2.4GHz" + fi + + # Get signal strength and speed + SIGNAL=$(iw dev $IFACE link 2>/dev/null | grep signal | awk '{print $2}') + RX_SPEED=$(iw dev $IFACE link 2>/dev/null | grep "rx bitrate" | awk '{print $3 " " $4}') + TX_SPEED=$(iw dev $IFACE link 2>/dev/null | grep "tx bitrate" | awk '{print $3 " " $4}') + + echo " 📶 Signal: $SIGNAL dBm ($(if [ "${SIGNAL:-0}" -ge -50 ]; then echo "Excellent"; elif [ "${SIGNAL:-0}" -ge -60 ]; then echo "Good"; elif [ "${SIGNAL:-0}" -ge -70 ]; then echo "Fair"; else echo "Poor"; fi))" + echo " 🚀 Speed: RX $RX_SPEED / TX $TX_SPEED" + else + echo " ❌ Not connected to any network" + CONNECTION_STATUS="DISCONNECTED" + fi + echo "" + + # Hardware capabilities + echo "🔧 HARDWARE STATUS" + echo "-----------------" + echo " Device: $(lspci | grep -i network | sed 's/^.*: //' | head -1)" + echo " Driver: $(ls -l /sys/class/net/$IFACE/device/driver/module 2>/dev/null | awk -F/ '{print $NF}' || echo "Unknown")" + echo " Firmware: $(rpm -q --queryformat '%{VERSION}-%{RELEASE}' linux-firmware 2>/dev/null || echo "Unknown")" + echo "" + + # Performance analysis + echo "⚡ PERFORMANCE ANALYSIS" + echo "---------------------" + POWER_SAVE=$(iw dev $IFACE get power_save 2>/dev/null | awk '{print $3}') + if [ "$POWER_SAVE" = "on" ]; then + echo " ⚠️ Power saving: ENABLED (may impact performance)" + else + echo " ✅ Power saving: DISABLED" + fi + + if [ "$CONNECTION_STATUS" = "SUBOPTIMAL" ] && [ "$SCAN_COUNT" -gt 0 ]; then + echo " ⚠️ Using slower 2.4GHz band but 5GHz networks available" + fi + + # Check for firmware errors + if run_with_privilege dmesg | grep -qi "error.*mt7921\|mt7921.*error"; then + echo " ⚠️ Firmware errors detected in system logs" + fi + echo "" + + # Recommendations + echo "💡 RECOMMENDATIONS" + echo "-----------------" + RECOMMENDATIONS=0 + + if [ "$POWER_SAVE" = "on" ]; then + echo " 1. Disable power saving for better performance:" + echo -e " \e[1;32msudo iw dev $IFACE set power_save off\e[0m # Only works for testing" + echo -e " To make it permanent: \e[1;32msudo nmcli connection modify \"$CURRENT_SSID\" wifi.powersave 2\e[0m" + RECOMMENDATIONS=$((RECOMMENDATIONS + 1)) + fi + + if [ "$CONNECTION_STATUS" = "SUBOPTIMAL" ] && [ "$SCAN_COUNT" -gt 0 ]; then + echo " $((RECOMMENDATIONS + 1)). Force 5GHz/6GHz band connection:" + echo -e " \e[1;32mnmcli connection modify \"$CURRENT_SSID\" wifi.band 5GHz\e[0m" + RECOMMENDATIONS=$((RECOMMENDATIONS + 1)) + fi + + # Check if no 5GHz networks are visible despite hardware support + # Check if no 5GHz networks are visible despite hardware support + if [ "${SCAN_COUNT:-0}" -eq 0 ] && \ + iw phy$(iw dev "$IFACE" | awk '/phy#/ {print $2}') info | grep -qE "5[0-9]{3}.*MHz"; then + echo " $((RECOMMENDATIONS + 1)). No 5GHz networks found! Troubleshooting steps:" + echo " a) Check if router has 5GHz enabled and broadcasting" + echo " b) Ensure router is in range (5GHz has shorter range than 2.4GHz)" + echo " c) Try disabling regulatory domain restrictions:" + echo -e " \e[1;32msudo iw reg set 00\e[0m # Temporary test only" + echo " d) Verify driver support with:" + echo -e " \e[1;32msudo modprobe -r mt7921e && sudo modprobe mt7921e\e[0m" + echo " e) If still not working, check dmesg for errors:" + echo -e " \e[1;32msudo dmesg | grep -i mt7921\e[0m" + RECOMMENDATIONS=$((RECOMMENDATIONS + 1)) + fi + + if run_with_privilege dmesg | grep -qi "error.*mt7921\|mt7921.*error"; then + echo " $((RECOMMENDATIONS + 1)). Update firmware to latest version:" + if [ "$IS_IMMUTABLE" -eq 1 ]; then + echo -e " \e[1;32msudo rpm-ostree upgrade\e[0m" + else + if command -v apt >/dev/null 2>&1; then + echo -e " \e[1;32msudo apt update && sudo apt upgrade linux-firmware\e[0m" + elif command -v dnf >/dev/null 2>&1; then + echo -e " \e[1;32msudo dnf upgrade linux-firmware\e[0m" + elif command -v pacman >/dev/null 2>&1; then + echo -e " \e[1;32msudo pacman -Syu linux-firmware\e[0m" + elif command -v zypper >/dev/null 2>&1; then + echo -e " \e[1;32msudo zypper update kernel-firmware\e[0m" + else + echo -e " \e[1;32mUpdate linux-firmware package using your distribution's package manager\e[0m" + fi + fi + RECOMMENDATIONS=$((RECOMMENDATIONS + 1)) + fi + + # Check for 5GHz connection failures + if journalctl -u NetworkManager --since "24 hours ago" 2>/dev/null | grep -qi "freq.*5[0-9][0-9][0-9].*fail"; then + echo " $((RECOMMENDATIONS + 1)). 5GHz connection failures detected. Try:" + echo " a) Reset network settings:" + echo -e " \e[1;32mnmcli connection delete \"$CURRENT_SSID\"\e[0m" + echo " Then reconnect to recreate profile" + echo " b) Disable MAC randomization:" + echo -e " \e[1;32mnmcli connection modify \"$CURRENT_SSID\" wifi.cloned-mac-address stable\e[0m" + echo " c) Use specific channel if DFS issues exist:" + echo -e " \e[1;32mnmcli connection modify \"$CURRENT_SSID\" wifi.channel 36\e[0m" + RECOMMENDATIONS=$((RECOMMENDATIONS + 1)) + fi + + if [ $RECOMMENDATIONS -eq 0 ]; then + echo " ✅ All systems optimal - no changes recommended!" + fi + echo "" + + # Quick statistics + echo "📊 QUICK STATS" + echo "-------------" + SCAN_COUNT=$(run_with_privilege iw dev $IFACE scan 2>/dev/null | grep -cE 'freq: [5-6][0-9][0-9][0-9]' || echo "0") + echo " Networks found: $SCAN_COUNT (5GHz/6GHz)" + echo " Connection quality: $CONNECTION_STATUS" + echo " Current band: ${BAND_TYPE:-Unknown}" + UPTIME=$(iw dev $IFACE station dump 2>/dev/null | grep "connected time" | awk '{print $3}') + if [ -n "$UPTIME" ]; then + echo " Connected for: $((UPTIME / 3600)) hours, $(( (UPTIME % 3600) / 60 )) minutes" + fi + echo "" + + # Final status + echo "+----------------------------------------+" + if [ "$CONNECTION_STATUS" = "EXCELLENT" ] || [ "$CONNECTION_STATUS" = "GOOD" ]; then + echo "| 🎉 WiFi Performance: $(printf "%-12s" "$CONNECTION_STATUS") 🎉 |" + elif [ "$CONNECTION_STATUS" = "SUBOPTIMAL" ]; then + echo "| ⚠️ WiFi Performance: $(printf "%-12s" "$CONNECTION_STATUS") ⚠️ |" + else + echo "| ❌ WiFi Status: $(printf "%-18s" "$CONNECTION_STATUS") ❌ |" + fi + echo "+----------------------------------------+" + else + echo "❌ No wireless interface detected!" + echo "" + echo "Please check your wireless hardware and drivers." + echo "+----------------------------------------+" + echo "| WiFi Status: NO INTERFACE |" + echo "+----------------------------------------+" + fi + +) | tee wifi_5ghz_diagnostic_$(date +%Y%m%d_%H%M%S).log diff --git a/Network-Diagnostic-Scripts/Ethernet-Diagnostic.sh b/Network-Diagnostic-Scripts/Ethernet-Diagnostic.sh new file mode 100644 index 0000000..7c81f03 --- /dev/null +++ b/Network-Diagnostic-Scripts/Ethernet-Diagnostic.sh @@ -0,0 +1,327 @@ +#!/bin/bash + +# USB-C Expansion Card Ethernet Diagnostic Script + +# Terminal formatting +if [ -t 1 ]; then + BOLD=$(tput bold) + YELLOW=$(tput setaf 3) + RESET=$(tput sgr0) +else + BOLD="" + YELLOW="" + RESET="" +fi + +# Log file for diagnostics +LOGFILE="$HOME/ethernet_diagnosis.log" + +# Global variables to store network environment data +ETH_INTERFACE="" +IP_ADDRESS="" +GATEWAY="" +LINK_SPEED="" +VPN_ACTIVE="" +VPN_TYPE="" + +# Function to log and display output +log_and_display() { + echo "${BOLD}${YELLOW}$@${RESET}" + echo "$@" >> "$LOGFILE" +} + +# Function to log and display output without formatting +log_and_display_plain() { + echo "$@" | tee -a "$LOGFILE" +} + +# Check for and remove previous log file +if [ -f "$LOGFILE" ]; then + log_and_display_plain "Removing previous diagnostic log file..." + rm "$LOGFILE" +fi + +# Function to detect the Linux distribution +detect_distro() { + if [ -f /etc/os-release ]; then + . /etc/os-release + echo "$ID" + else + echo "Unknown" + fi +} + +# Function to install necessary packages quietly if not already installed +install_packages() { + distro=$(detect_distro) + case $distro in + ubuntu) + sudo apt-get update -qq > /dev/null 2>&1 + sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq network-manager iproute2 speedtest-cli usbutils ethtool inxi > /dev/null 2>&1 + ;; + fedora) + sudo dnf install -y -q NetworkManager iproute speedtest-cli usbutils ethtool inxi > /dev/null 2>&1 + ;; + *) + echo "Unsupported distribution: $distro" + exit 1 + ;; + esac +} + +# Function to provide deep insights +provide_insights() { + log_and_display_plain "Ethernet Interface: $1" + log_and_display_plain "Detailed Network Information:" + nmcli device show "$1" | grep -E 'GENERAL.STATE|IP4.ADDRESS|IP4.GATEWAY' + log_and_display_plain "Network Route Information:" + ip route show dev "$1" +} + +# Function to display a progress bar +show_progress() { + local duration=$1 + local steps=20 + local sleep_duration=$(echo "scale=2; $duration / $steps" | bc) + + for ((i=0; i<=steps; i++)); do + local percentage=$((i * 100 / steps)) + local completed=$((i * 20 / steps)) + local remaining=$((20 - completed)) + printf "\r[%-20s] %d%%" "$(printf '#%.0s' $(seq 1 $completed))$(printf ' %.0s' $(seq 1 $remaining))" "$percentage" + sleep $sleep_duration + done + echo +} + +# Function to run speed test +run_speedtest() { + log_and_display_plain "Running speed test...(may appear to stop at 75% which is normal)" + show_progress 30 & + progress_pid=$! + speedtest_output=$(speedtest-cli --simple 2>/dev/null) + kill $progress_pid 2>/dev/null + wait $progress_pid 2>/dev/null + printf "\033[1A\033[K" # Move cursor up and clear the line + log_and_display_plain "Speed Test Results:" + echo "$speedtest_output" | while IFS= read -r line; do + log_and_display_plain "$line" + done + + # Extract values for later use + ping=$(echo "$speedtest_output" | awk '/Ping:/ {print $2}') + download=$(echo "$speedtest_output" | awk '/Download:/ {print $2}') + upload=$(echo "$speedtest_output" | awk '/Upload:/ {print $2}') +} + +# Function to check Ethernet interfaces +check_ethernet_interfaces() { + log_and_display_plain "Ethernet Interface Information:" + ETH_INTERFACE=$(ip -o link show | awk -F': ' '$2 ~ /^en|^eth/{print $2; exit}') + + if [ -z "$ETH_INTERFACE" ]; then + log_and_display_plain "No Ethernet interface found." + return + fi + + log_and_display_plain "Interface: $ETH_INTERFACE" + + # Get IP address and gateway + IP_ADDRESS=$(ip -4 addr show $ETH_INTERFACE | grep -oP '(?<=inet\s)\d+(\.\d+){3}') + GATEWAY=$(ip route | awk '/default/ && /'$ETH_INTERFACE'/ {print $3}') + + log_and_display_plain "IP Address: $IP_ADDRESS" + log_and_display_plain "Gateway: $GATEWAY" + + # Get link speed + LINK_SPEED=$(sudo ethtool $ETH_INTERFACE 2>/dev/null | awk '/Speed:/ {print $2}') + log_and_display_plain "Link Speed: $LINK_SPEED" + + # Check for VPN (tun0 or wg0) interface + if ip link show tun0 &>/dev/null; then + VPN_ACTIVE="Yes" + VPN_TYPE="OpenVPN" + log_and_display_plain "VPN (tun0) detected: Active (OpenVPN)" + elif ip link show wg0 &>/dev/null; then + VPN_ACTIVE="Yes" + VPN_TYPE="WireGuard" + log_and_display_plain "VPN (wg0) detected: Active (WireGuard)" + else + VPN_ACTIVE="No" + VPN_TYPE="None" + log_and_display_plain "VPN detected: Not active" + fi +} + +# Function to summarize findings +summarize_findings() { + log_and_display_plain + log_and_display "Ethernet Diagnostic Summary" + log_and_display "============================" + + log_and_display_plain "- Ethernet Interface: $ETH_INTERFACE" + log_and_display_plain "- IP Address: $IP_ADDRESS" + log_and_display_plain "- Gateway: $GATEWAY" + log_and_display_plain "- Link Speed: $LINK_SPEED" + log_and_display_plain "- VPN Active: $VPN_ACTIVE" + if [ "$VPN_ACTIVE" == "Yes" ]; then + log_and_display_plain "- VPN Type: $VPN_TYPE" + fi + + log_and_display_plain "Speed Test Results:" + log_and_display_plain "- Ping: ${ping} ms" + log_and_display_plain "- Download: ${download} Mbit/s" + log_and_display_plain "- Upload: ${upload} Mbit/s" + + log_and_display "INSIGHTS" + log_and_display "========" + + # Ethernet Interface INSIGHTS + log_and_display_plain "Ethernet Interface:" + if [ -n "$ETH_INTERFACE" ]; then + log_and_display_plain "- Connected via interface: $ETH_INTERFACE" + + speed_value=$(echo "$LINK_SPEED" | sed 's/[^0-9]*//g') + if [ -n "$speed_value" ]; then + if [ "$speed_value" -ge 1000 ]; then + log_and_display_plain "- Excellent link speed ($LINK_SPEED). This is suitable for very high-bandwidth activities." + elif [ "$speed_value" -ge 100 ]; then + log_and_display_plain "- Good link speed ($LINK_SPEED). This is suitable for most high-bandwidth activities." + else + log_and_display_plain "- Lower link speed ($LINK_SPEED). You might experience slowdowns with high-bandwidth activities." + fi + else + log_and_display_plain "- Unable to determine link speed." + fi + else + log_and_display_plain "- No active Ethernet interface detected." + fi + + # Speed Test INSIGHTS + log_and_display_plain "Speed Test:" + if [[ -n "$ping" && -n "$download" && -n "$upload" ]]; then + if (( $(echo "$ping < 20" | bc -l) )); then + log_and_display_plain "- Excellent ping time. Great for real-time applications like gaming or video calls." + elif (( $(echo "$ping < 50" | bc -l) )); then + log_and_display_plain "- Good ping time. Suitable for most online activities." + else + log_and_display_plain "- Higher ping time. You might experience lag in real-time applications." + fi + + if (( $(echo "$download > 100" | bc -l) )); then + log_and_display_plain "- Fast download speed (${download} Mbit/s). Excellent for streaming, large file downloads, and multiple users. Note: Speed test results should be compared to other services such as Fast.com and Google Speed Test if the results feel wrong." + elif (( $(echo "$download > 25" | bc -l) )); then + log_and_display_plain "- Good download speed (${download} Mbit/s). Suitable for most online activities and HD streaming. Note: Speed test results should be compared to other services such as Fast.com and Google Speed Test if the results feel wrong." + else + log_and_display_plain "- Lower download speed (${download} Mbit/s). You might experience buffering with HD streaming or slow file downloads. Note: Speed test results should be compared to other services such as Fast.com and Google Speed Test if the results feel wrong." + fi + + if (( $(echo "$upload > 20" | bc -l) )); then + log_and_display_plain "- Good upload speed (${upload} Mbit/s). Suitable for video calls, uploading large files, and online backups." + else + log_and_display_plain "- Lower upload speed (${upload} Mbit/s). You might experience issues with video calls or uploading large files." + fi + else + log_and_display_plain "- Speed test results not available. Unable to provide insights on network performance." + fi + + # VPN INSIGHTS + log_and_display_plain "VPN Status:" + if [ "$VPN_ACTIVE" == "Yes" ]; then + log_and_display_plain "- A VPN ($VPN_TYPE) is active. This may impact your internet speed and latency, but provides increased privacy and security." + else + log_and_display_plain "- No VPN detected. Your internet traffic is not being routed through a VPN at the moment." + fi + + log_and_display_plain +} + +# Function to perform system information check +check_system_info() { + log_and_display_plain "System Information:" + log_and_display_plain "Operating System: $(cat /etc/os-release 2>/dev/null | grep PRETTY_NAME | cut -d'"' -f2)" + log_and_display_plain "Kernel Version: $(uname -r)" + log_and_display_plain "CPU: $(lscpu 2>/dev/null | grep "Model name" | cut -d':' -f2 | xargs)" + log_and_display_plain "Total RAM: $(free -h 2>/dev/null | awk '/^Mem:/{print $2}')" + log_and_display_plain "Last System Update: $(ls -lct /var/log 2>/dev/null | grep -E "dpkg|dnf" | tail -1 | awk '{print $6, $7, $8}')" + + # Added USB Device Information + log_and_display_plain "USB Device Information:" + sudo lsusb | grep -E 'Ethernet|LAN' + + log_and_display_plain +} + +# Main function to run all checks +run_diagnostics() { + check_ethernet_interfaces + run_speedtest + provide_insights "$ETH_INTERFACE" + summarize_findings +} + +# Script execution starts here +log_and_display "USB-C Expansion Card Ethernet Diagnostic Script" +log_and_display "-------------------------------------" + +# Auto-detect distro and install needed packages +install_packages + +# Check system information +check_system_info + +# Get the Ethernet interface name +ETH_INTERFACE=$(nmcli device status | grep ethernet | awk '{print $1}') + +# Check if Ethernet interface was found +if [ -z "$ETH_INTERFACE" ]; then + log_and_display_plain "No Ethernet interface found. Please ensure your Ethernet device is connected." + log_and_display_plain "Checking for network devices..." + sudo lspci | grep -E 'Ethernet' + sudo lsusb | grep -E 'Ethernet|LAN' + exit 1 +fi + +# Check if Ethernet is connected +eth_status=$(nmcli device status | grep "$ETH_INTERFACE" | awk '{print $3}') + +if [[ "$eth_status" != "connected" ]]; then + log_and_display_plain "Ethernet is not connected or online." + provide_insights "$ETH_INTERFACE" +else + log_and_display_plain "Ethernet is connected." + log_and_display_plain "Starting Ethernet Diagnostics..." + run_diagnostics +fi + +# Perform ping test +log_and_display_plain "Ping Results:" +ping_result=$(ping -c 4 8.8.8.8) +log_and_display_plain "$ping_result" + +# Final summary +log_and_display "${YELLOW}Final Recommendations${RESET}" +log_and_display "${YELLOW}=====================${RESET}" + +if [[ "$eth_status" == "connected" ]]; then + log_and_display_plain "• Your Ethernet connection speed ($LINK_SPEED) is excellent. It should handle all types of internet activities without any issues." + log_and_display_plain "• Your download speed (${download} Mbit/s) is good and should handle most online activities well, though it's not fully utilizing your Ethernet link speed." + log_and_display_plain "• Your upload speed (${upload} Mbit/s) is good and should handle most upload tasks well, though it's not fully utilizing your Ethernet link speed." + log_and_display_plain "• Your ping (${ping} ms) is good and should provide a responsive experience for most applications." + log_and_display_plain "• Note: Your actual internet speeds (${download} Mbit/s down / ${upload} Mbit/s up) are lower than your Ethernet link speed ($LINK_SPEED). This is normal, as your internet speed is typically limited by your ISP plan, not your local network capability." + log_and_display_plain "• Regularly update your network drivers and router firmware to ensure optimal performance." + log_and_display_plain "• If speeds are consistently lower than expected, contact your ISP to check for any line issues." +else + log_and_display_plain "• Your Ethernet connection is not active. Please check your cable connection and network settings." +fi + +if [ "$VPN_ACTIVE" == "Yes" ]; then + log_and_display_plain "• Note that your active VPN ($VPN_TYPE) may impact your internet speeds and latency. For accurate network testing, consider temporarily disabling your VPN." +fi + +# End of script +log_and_display "${YELLOW}Ethernet Diagnostics Completed${RESET}" +log_and_display "${YELLOW}===============================${RESET}" +log_and_display_plain "Thank you for using this diagnostic tool." +log_and_display_plain "If you continue to experience issues, consider testing different cables and using different expansion slots to isolate where the issue is taking place." +log_and_display_plain "Complete diagnostic results have been saved to: ${YELLOW}$LOGFILE${RESET}" diff --git a/Network-Diagnostic-Scripts/README.md b/Network-Diagnostic-Scripts/README.md new file mode 100644 index 0000000..bb54312 --- /dev/null +++ b/Network-Diagnostic-Scripts/README.md @@ -0,0 +1,183 @@ +# Ethernet and Wi-Fi Diagnostic Scripts + + +### Install Curl + +Curl should already be installed, but just in case: + +### Fedora +``` +sudo dnf install curl -y +``` + +or + +### Ubuntu +``` +sudo apt install curl -y +``` +  +  +  + + +**Looking for the [Wi-Fi Diagnostic tool](https://github.com/FrameworkComputer/linux-docs/tree/main/Network-Diagnostic-Scripts#to-install-wi-fi-diagnostic-script-simply-run)? Click here to scroll down.** + +**Looking for the [Frequency Diagnostic Tool](https://github.com/FrameworkComputer/linux-docs/tree/main/Network-Diagnostic-Scripts#to-install-5ghz6ghz-frequency-diagnostic-tool-simply-run)? Click here to scroll down.** + + >Use Wifi‑Diagnostic.sh when you want a broad view of your Wi‑Fi health (interfaces, speed, VPN, overall environment). + + >Use 5ghz‑diag.sh when you need to deeply troubleshoot why your system isn’t seeing or staying on 5 GHz/6 GHz (reg domains, PHY capabilities, DFS, band‑steering, driver/firmware issues). + +  + + +------------------------------------------------------------------------------------------------------------------------------ + +## To Install Ethernet Diagnostic Script, simply run: +``` +curl -s https://raw.githubusercontent.com/FrameworkComputer/linux-docs/main/Network-Diagnostic-Scripts/Ethernet-Diagnostic.sh -o Ethernet-Diagnostic.sh && clear && bash Ethernet-Diagnostic.sh +``` + +Running the script in the future +After the install, you can run going forward with the following in the HOME directory. So merely opening a terminal and running this will work if the original script has not been moved. + +``` +bash Ethernet-Diagnostic.sh +``` +![Ethernet-Diagnostic](https://raw.githubusercontent.com/FrameworkComputer/linux-docs/main/Network-Diagnostic-Scripts/images/Ethernet-Diag.png) + +- Detects and installs necessary packages based on the Linux distribution +- Creates a log file for storing diagnostic results +- Formats output with bold and yellow text for better readability +- Detects Ethernet interfaces and retrieves network information +- Checks IP address and gateway +- Determines link speed +- Detects active VPN connections (OpenVPN or WireGuard) +- Runs a speed test using speedtest-cli +- Provides a progress bar for the speed test +- Summarizes findings including Ethernet interface, IP, gateway, link speed, and VPN status +- Offers insights on network performance based on speed test results +- Checks and displays system information (OS, kernel, CPU, RAM, last update) +- Lists USB devices, focusing on Ethernet/LAN devices +- Performs a ping test to 8.8.8.8 +- Provides final recommendations based on the diagnostic results +- Compares actual internet speeds with Ethernet link speed +- Suggests updating network drivers and router firmware +- Advises contacting ISP if speeds are consistently lower than expected +- Notes the potential impact of VPN on network performance +- Saves complete diagnostic results to a log file +- Suggests further troubleshooting steps (testing cables and expansion slots) + +------------------------------------------------------------------------------------------------------------------------------ + + + + +## To Install Wi-Fi Diagnostic Script, simply run: +``` +curl -s https://raw.githubusercontent.com/FrameworkComputer/linux-docs/main/Network-Diagnostic-Scripts/Wifi-Diagnostic.sh -o Wifi-Diagnostic.sh && clear && bash Wifi-Diagnostic.sh +``` + +Running the script in the future +After the install, you can run going forward with the following in the HOME directory. So merely opening a terminal and running this will work if the original script has not been moved. + +``` +bash Wifi-Diagnostic.sh +``` +![Wi-Fi-Diagnostic](https://raw.githubusercontent.com/FrameworkComputer/linux-docs/main/Network-Diagnostic-Scripts/images/WiFi_Diag.png) + +- Detects and installs necessary packages based on the Linux distribution (Ubuntu or Fedora) +- Creates a log file for storing diagnostic results +- Formats output with bold and yellow text for better readability +- Checks for internet connectivity with a ping test to 8.8.8.8 +- Provides rfkill status and recommendations if no network is detected +- Checks and installs required tools if missing +- Performs system information check (OS, kernel, CPU, RAM, last update) +- Detects Wi-Fi card information +- Checks Wi-Fi interfaces and retrieves detailed network information +- Monitors Wi-Fi signal strength and quality +- Detects active VPN connections (OpenVPN or WireGuard) +- Performs a network speed test using speedtest-cli +- Displays a loading bar during the speed test +- Checks network status and connection speed using nmcli and iw +- Summarizes findings including Wi-Fi interface, speed test results, and network environment +- Provides insights on system information, Wi-Fi interface, speed test results, and network environment +- Analyzes signal strength, transmission rate, and connection quality +- Interprets speed test results (ping, download, and upload speeds) +- Checks and explains power save mode status +- Provides VPN-specific insights if a VPN is active +- Suggests further actions based on diagnostic results +- Saves complete diagnostic results to a log file + +------------------------------------------------------------------------------------------------------------------------------ + +## To Install 5Ghz/6Ghz Frequency Diagnostic Tool, simply run: +**Important:** The images below are merely the bottom half of the data piped out into a log file into your home directory. They only reflect a brief summary of the findings. +You cam take the completed log to get help from the support team. + +This script is a comprehensive diagnostic tool specifically designed to troubleshoot issues related to 5GHz (and 6GHz) WiFi connections on Linux systems, including those using immutable distributions like Bazzite and Project Bluefin. It automates the collection of detailed information about your wireless hardware, software configuration, and network environment to help identify potential problems preventing successful connection or optimal performance on faster WiFi bands. + + +``` +curl -s https://raw.githubusercontent.com/FrameworkComputer/linux-docs/refs/heads/main/Network-Diagnostic-Scripts/5ghz-diag.sh -o 5ghz-diag.sh && clear && sudo bash 5ghz-diag.sh +``` + +Running the tool in the future +After the install, you can run going forward with the following in the HOME directory. So merely opening a terminal and running this will work if the original script has not been moved. + + +``` +sudo bash 5ghz-diag.sh +``` + +![Frequency Diagnostic Script](https://raw.githubusercontent.com/FrameworkComputer/linux-docs/refs/heads/main/Network-Diagnostic-Scripts/images/5gz1.png) + +![Frequency Diagnostic Script](https://raw.githubusercontent.com/FrameworkComputer/linux-docs/refs/heads/main/Network-Diagnostic-Scripts/images/5gz2.png) + + + +- Checks for pending reboots: Determines if a system reboot is required, especially relevant after installing dependencies on immutable systems. +- Verifies sudo privileges: Checks if the script is run with administrative privileges, noting that some features require sudo for full functionality. +- Installs missing dependencies: Identifies and offers to install necessary command-line tools (iw, nmcli, ip) using the appropriate package manager for your distribution (immutable or traditional). +- Displays Regulatory Domain Information: Shows the configured wireless regulatory domain, which can impact available channels and power levels. +- Detects Network Interface: Identifies your primary wireless network interface. +- Reports Hardware Capabilities: Provides details about your wireless card, including supported WiFi bands (2.4GHz, 5GHz, 6GHz), frequencies, and features (like HT, VHT, HE, 160MHz, DFS). +- Shows Current Connection Status: Displays information about your current WiFi connection, including SSID, frequency, signal strength, and transfer rates. +- Scans for 5GHz Networks: Lists available 5GHz (and 6GHz) wireless networks in your vicinity, including their frequency, signal strength, security type, and BSSID (requires sudo). +- Lists 5GHz Channel Availability: Shows the 5GHz channels supported by your hardware. +- Examines NetworkManager Configuration: Displays relevant NetworkManager settings, including WiFi radio status, device capabilities, and the configuration of your current connection profile. +- Provides Firmware Information: Reports the version of the linux-firmware package and attempts to show loaded wireless firmware files and recent firmware-related messages from the system log (dmesg). +- Details Driver and Module Information: Shows the wireless driver in use, its parameters, and general module information. +- Analyzes Connection Events: Extracts recent connection attempts, successes, and failures related to NetworkManager and wpa_supplicant from the system journal. +- Reviews Recent Errors and Warnings: Filters the system journal for recent errors and warnings related to NetworkManager and wpa_supplicant. +- Checks Power Management Settings: Reports the current WiFi power saving status and checks for relevant configurations in TLP if installed. +- Investigates Band Steering and Roaming Settings: Displays information about BSS transition management capabilities and NetworkManager's band selection configuration. +- Identifies 5GHz Connection Issues: Performs specific checks to diagnose common 5GHz connection problems, such as hardware support, current connection band, visibility of 5GHz networks, band selection settings, DFS issues, and driver/firmware errors. +- Generates a Summary and Recommendations: Provides a concise summary of the connection status, hardware details, and performance analysis, along with tailored recommendations based on the findings. + + ### Important +- **Logs Output**: Saves the entire diagnostic report to a timestamped log file. + + + +------------------------------------------------------------------------------------------------------------------------------ + + +### FAQ + +- Does this fix anything with networking? +>This is a tool to give you and if need be, Framework Support an idea of what your network looks like. It also provides critical guidence on power save state, VPN usage, speed testing results, and overall network environment which may indicate where a problem is happening. + + +- Will this tell me why my network is so slow? +>Indirectly, yes. It can indicate items such as frequency, crowded wifi channels, among a number of other variables to examine. VPNs are a huge one. + +- Does this support Project Bluefin or Bazzite? +>I have specific scripts in testing for these distros. We will be using Homebrew for them. Will update this repo when they are ready. + +- Does this work for a laptop whereas I cannot use the internet at all? +>No. This is for performance issues specifically. That said, if you download the file onto another computer, move it to the Framework Laptop that is lacking internet, the wifi script will provide some hints as to what might be going on and how to fix it. + +- My wifi card keeps dropping out. +> Please use [this script](https://github.com/FrameworkComputer/network-tester?tab=readme-ov-file#mediatekintel-wi-fi-drop-tester), run it for one hour. It will tell you when it's done running. Grab both the ping_logfile.log and iw_logfile.log logs it creates in your home directory and send it to support. This should capture the timeout and indicate if there was a signal drop, frequency change or another event. diff --git a/Network-Diagnostic-Scripts/Wifi-Diagnostic.sh b/Network-Diagnostic-Scripts/Wifi-Diagnostic.sh new file mode 100644 index 0000000..dc1fb71 --- /dev/null +++ b/Network-Diagnostic-Scripts/Wifi-Diagnostic.sh @@ -0,0 +1,535 @@ +#!/bin/bash + +# Terminal formatting +if [ -t 1 ]; then + BOLD=$(tput bold) + YELLOW=$(tput setaf 3) + RESET=$(tput sgr0) +else + BOLD="" + YELLOW="" + RESET="" +fi + +# Function to print the script title in yellow and bold at the top of the terminal +print_title() { + echo "${BOLD}${YELLOW}Integrated Wi-Fi Diagnostic Script${RESET}" + echo "---------------------------------" +} + +# Ensure necessary packages are installed on Ubuntu and Fedora before proceeding +if [ -f /etc/os-release ]; then + . /etc/os-release + case "$ID" in + ubuntu) + sudo apt-get update -qq + sudo apt-get install -y -qq pciutils iw inxi speedtest-cli || { echo "${YELLOW}Package installation failed on Ubuntu.${RESET}"; exit 1; } + ;; + fedora) + sudo dnf install -y -q pciutils iw inxi speedtest-cli || { echo "${YELLOW}Package installation failed on Fedora.${RESET}"; exit 1; } + ;; + *) + echo "${YELLOW}Unsupported distribution: $ID${RESET}" + exit 1 + ;; + esac +else + echo "${YELLOW}Could not detect the OS distribution.${RESET}" + exit 1 +fi + +# Clear the screen and ensure title is always printed at the top +clear +print_title + +# Configuration for thresholds +SIGNAL_THRESHOLD=-65 +QUALITY_THRESHOLD=50 + +# Log file for diagnostics +LOGFILE="$HOME/wifi_diagnosis.log" + +# Global variables to store network environment data +SSID="" +SIGNAL_STRENGTH="" +SIGNAL_LEVEL="" +WIFI_FREQUENCY="" +VPN_ACTIVE="" +VPN_TYPE="" +VPN_INTERFACE="" +POWER_SAVE="" + +# Function to log and display output +log_and_display() { + echo "${BOLD}${YELLOW}$@${RESET}" + echo "$@" >> "$LOGFILE" +} + +# Function to log and display output without formatting +log_and_display_plain() { + echo "$@" | tee -a "$LOGFILE" +} + +# Check for and remove previous log file +if [ -f "$LOGFILE" ]; then + log_and_display_plain "Removing previous diagnostic log file..." + rm "$LOGFILE" +fi + +# Check for internet connectivity and display ping results +check_internet_connection() { + log_and_display_plain "Performing ping test to 8.8.8.8..." + PING_RESULTS=$(ping -c 4 8.8.8.8 2>&1) + echo "$PING_RESULTS" >> "$LOGFILE" + log_and_display_plain "$PING_RESULTS" + log_and_display_plain + if echo "$PING_RESULTS" | grep -q "0% packet loss"; then + return 0 + else + log_and_display_plain "No network detected. Exiting." + log_and_display_plain "rfkill Status:" + RFKILL_STATUS=$(rfkill list) + log_and_display_plain "$RFKILL_STATUS" + + log_and_display "Final Recommendations" + log_and_display "=====================" + + # Explain rfkill status + log_and_display_plain "rfkill status explanation:" + if echo "$RFKILL_STATUS" | grep -q "Soft blocked: yes"; then + log_and_display_plain "- Soft blocked: Yes. This means the Wi-Fi is disabled by software. To unblock, run: ${YELLOW}rfkill unblock wifi${RESET}" + fi + if echo "$RFKILL_STATUS" | grep -q "Hard blocked: yes"; then + log_and_display_plain "- Hard blocked: Yes. This means the Wi-Fi is disabled by a physical switch. Check for a Wi-Fi switch on your device and turn it on." + fi + if echo "$RFKILL_STATUS" | grep -q "Soft blocked: no" && echo "$RFKILL_STATUS" | grep -q "Hard blocked: no"; then + log_and_display_plain "- Wi-Fi is not blocked by rfkill. The issue might be related to drivers or hardware." + fi + + log_and_display_plain "- After addressing these issues, rerun the script to perform a full diagnosis." + exit 1 + fi +} + +# Function to run commands with elevated privileges if available +run_elevated() { + if command -v sudo >/dev/null 2>&1; then + sudo "$@" + elif command -v doas >/dev/null 2>&1; then + doas "$@" + else + log_and_display_plain "Neither sudo nor doas is available. Some features may be limited." + "$@" + fi +} + +# Function to check if a command/package is installed +is_installed() { + command -v "$1" >/dev/null 2>&1 +} + +# Function to check and install required tools +check_and_install_tools() { + tools="iw speedtest-cli dig ping lspci ethtool nmcli systemctl resolvectl net-tools dmesg grep awk sed bc" + missing_tools="" + + for tool in $tools; do + if ! is_installed "$tool"; then + missing_tools="$missing_tools $tool" + fi + done + + if [ -z "$missing_tools" ]; then + log_and_display_plain "All required tools are already installed." + return + fi + + log_and_display_plain "The following tools need to be installed:$missing_tools" + for tool in $missing_tools; do + install_package "$tool" + done + + log_and_display_plain "Tool installation complete." + # Clear the screen after tool installation and re-print the title + clear + print_title +} + +# Function to perform system information check +check_system_info() { + log_and_display_plain "System Information:" + log_and_display_plain "Operating System: $(cat /etc/os-release 2>/dev/null | grep PRETTY_NAME | cut -d'"' -f2)" + log_and_display_plain "Kernel Version: $(uname -r)" + log_and_display_plain "CPU: $(lscpu 2>/dev/null | grep 'Model name' | cut -d':' -f2 | xargs)" + log_and_display_plain "Total RAM: $(free -h 2>/dev/null | awk '/^Mem:/{print $2}')" + log_and_display_plain "Last System Update: $(ls -lct /var/log 2>/dev/null | grep -E 'dpkg|dnf' | tail -1 | awk '{print $6, $7, $8}')" + log_and_display_plain "Wi-Fi Card installed: $(run_elevated lspci | grep -E 'Network controller')" +} + +# Function to check Wi-Fi interfaces +check_wifi_interfaces() { + log_and_display_plain "Wi-Fi Interface Information:" + iw dev 2>/dev/null | awk '$1=="Interface"{print $2}' | while read -r interface; do + log_and_display_plain "Interface: $interface" + iw dev "$interface" info 2>/dev/null | tee -a "$LOGFILE" + link_info=$(iw dev "$interface" link 2>/dev/null) + log_and_display_plain "$link_info" + + if echo "$link_info" | grep -q "Not connected"; then + SSID="N/A" + SIGNAL_STRENGTH="N/A" + WIFI_FREQUENCY="N/A" + else + SSID=$(echo "$link_info" | awk '/SSID/{print $2}') + SIGNAL_STRENGTH=$(echo "$link_info" | awk '/signal/{print $2 " " $3}') + WIFI_FREQUENCY=$(echo "$link_info" | awk '/freq/{print $2}') + + freq=$(echo "$link_info" | awk '/freq/{print $2}') + + signal=$(echo "$link_info" | awk '/signal/{print $2}') + log_and_display_plain "Signal Strength: $signal dBm" + signal_abs=$(echo "$signal" | tr -d '-') + + tx_bitrate=$(echo "$link_info" | awk '/tx bitrate/{print $3}') + log_and_display_plain "Transmission Rate: $tx_bitrate" + fi + + # Check power save mode + power_save=$(iw dev "$interface" get power_save 2>/dev/null) + if [ $? -eq 0 ]; then + POWER_SAVE="$power_save" + log_and_display_plain "$POWER_SAVE" + else + POWER_SAVE="Unable to determine" + log_and_display_plain "$POWER_SAVE" + fi + + # Write POWER_SAVE to log file for later retrieval + echo "POWER_SAVE=$POWER_SAVE" >> "$LOGFILE" + + log_and_display_plain + done + + # Check for VPN (tun0 or wg0) interfaces + if ip link show tun0 &>/dev/null; then + VPN_ACTIVE="Yes" + VPN_TYPE="OpenVPN" + VPN_INTERFACE="tun0" + log_and_display_plain "VPN (tun0) detected: Active (OpenVPN)" + elif ip link show wg0 &>/dev/null; then + VPN_ACTIVE="Yes" + VPN_TYPE="WireGuard" + VPN_INTERFACE="wg0" + log_and_display_plain "VPN (wg0) detected: Active (WireGuard)" + else + VPN_ACTIVE="No" + VPN_TYPE="None" + VPN_INTERFACE="None" + log_and_display_plain "VPN (tun0/wg0) detected: Not active" + fi +} + +# Function to display a loading bar +show_loading_bar() { + local duration=$1 + local width=50 + local interval=0.1 + local progress=0 + local full_bar=$(printf "%${width}s" | tr ' ' '=') + + while [ $progress -lt $width ]; do + local bar=$(printf "%.*s" $progress "$full_bar") + printf "\r[%-${width}s] %d%%" "$bar" $((progress*2)) + sleep $interval + progress=$((progress+1)) + done + printf "\n" +} + +# Function to perform speed test +perform_speed_test() { + log_and_display_plain "Network Speed Test:" + log_and_display_plain "Running speed test, please wait... ${YELLOW}(Note: The test may appear to hang at 98% for a few minutes. This is normal - wait patiently.)${RESET}" + + # Start the loading bar in the background + show_loading_bar 60 & + loading_pid=$! + + # Run the speed test + speed_test=$(speedtest-cli --secure 2>/dev/null) + speed_test_exit_code=$? + + # Stop the loading bar + kill $loading_pid 2>/dev/null + wait $loading_pid 2>/dev/null + printf "\r%$(tput cols)s\r" # Clear the line + + if [ $speed_test_exit_code -eq 0 ]; then + log_and_display_plain "$speed_test" + + ping=$(echo "$speed_test" | awk '/Ping/{print $2}') + download=$(echo "$speed_test" | awk '/Download/{print $2}') + upload=$(echo "$speed_test" | awk '/Upload/{print $2}') + + log_and_display_plain + else + log_and_display_plain "Failed to perform speed test. Please check your internet connection and try again." + fi + log_and_display_plain +} + +# Function to check network status with nmcli +check_nmcli_status() { + log_and_display_plain "Checking network status with nmcli..." + nmcli dev status 2>/dev/null | tee -a "$LOGFILE" + nmcli dev wifi list 2>/dev/null | tee -a "$LOGFILE" + + # Check connection speed using iw + connection_speed=$(iw dev $(iw dev | awk '$1=="Interface"{print $2}') link | grep 'tx bitrate' | awk '{print $3, $4}') + + if [[ -n "$connection_speed" ]]; then + log_and_display_plain "Current connection speed: $connection_speed" + + # Extract the numeric value from the speed string (handle decimals correctly) + speed_value=$(echo "$connection_speed" | awk '{print int($1)}') + if (( speed_value < 20 )); then + log_and_display_plain "Note: Connection speed is below 20 Mbit/s" + fi + else + log_and_display_plain "No active Wi-Fi connection detected or speed not available." + fi +} + +# Function to monitor signal and quality using nmcli +monitor_signal_quality() { + log_and_display_plain "Monitoring Wi-Fi signal and quality with nmcli..." + + # Get active Wi-Fi connection details + wifi_info=$(nmcli -f IN-USE,SSID,SIGNAL dev wifi list 2>/dev/null | grep '^*') + if [[ -z "$wifi_info" ]]; then + log_and_display_plain "No active Wi-Fi connection found." + SIGNAL_LEVEL="N/A" + return + fi + + # Extract signal strength and quality + SSID=$(echo "$wifi_info" | awk '{print $2}') + SIGNAL_LEVEL=$(echo "$wifi_info" | awk '{print $NF}') + + log_and_display_plain "Connected to SSID: $SSID" + log_and_display_plain "Signal level: ${SIGNAL_LEVEL:-N/A}%" + + if [[ -n "$SIGNAL_LEVEL" && "$SIGNAL_LEVEL" =~ ^[0-9]+$ ]]; then + if [ "$SIGNAL_LEVEL" -lt "$QUALITY_THRESHOLD" ]; then + log_and_display_plain "Note: Signal quality is below ${QUALITY_THRESHOLD}%" + fi + fi +} + +# Function to summarize findings +summarize_findings() { + log_and_display_plain + log_and_display "Wi-Fi Diagnostic Summary" + log_and_display "========================" + + # Summarize Wi-Fi interface information + log_and_display_plain "Wi-Fi Interface:" + iw dev 2>/dev/null | awk '$1=="Interface"{print "- " $2}' | tee -a "$LOGFILE" + + # Summarize speed test results + log_and_display_plain "Speed Test Results:" + if [[ -f "$LOGFILE" ]]; then + ping=$(grep "Ping:" "$LOGFILE" 2>/dev/null | tail -n1 | awk '{print $2}') + download=$(grep "Download:" "$LOGFILE" 2>/dev/null | tail -n1 | awk '{print $2}') + upload=$(grep "Upload:" "$LOGFILE" 2>/dev/null | tail -n1 | awk '{print $2}') + if [[ -n "$ping" && -n "$download" && -n "$upload" ]]; then + log_and_display_plain "- Ping: ${ping} ms" + log_and_display_plain "- Download: ${download} Mbps" + log_and_display_plain "- Upload: ${upload} Mbps" + else + log_and_display_plain "Speed test results not available. The test may have failed or not been performed." + fi + else + log_and_display_plain "Speed test results not available. The log file may be missing." + fi + + # Add Network Environment section + # Detect the Wi-Fi interface if not set + if [ -z "$interface" ]; then + interface=$(iw dev | awk '$1=="Interface"{print $2; exit}') + fi + + # Get link information + link_info=$(iw dev "$interface" link) + + # Extract the frequency + frequency2=$(iw dev "$interface" link | awk '/freq/ {print $2}') + + signal_strength=$(iw dev "$interface" link | awk '/signal:/ {print $2}') + + # Retrieve POWER_SAVE from log file + POWER_SAVE=$(grep "POWER_SAVE=" "$LOGFILE" | tail -n1 | cut -d'=' -f2) + + # Display the network environment information + log_and_display "Network Environment" + log_and_display "====================" + log_and_display_plain "- SSID: $SSID" + log_and_display_plain "- Signal Strength: $signal_strength dBm" + log_and_display_plain "- Signal level: $SIGNAL_LEVEL%" + log_and_display_plain "- Frequency: $frequency2 MHz" + log_and_display_plain "- $POWER_SAVE" + log_and_display_plain "- VPN Active: $VPN_ACTIVE" + if [ "$VPN_ACTIVE" == "Yes" ]; then + log_and_display_plain "- VPN Type: $VPN_TYPE ($VPN_INTERFACE)" + fi + + log_and_display_plain + + # Add INSIGHTS section + log_and_display "INSIGHTS" + log_and_display "========" + + # System Information INSIGHTS + log_and_display_plain "System Information:" + log_and_display_plain "- Your system specifications are important for Wi-Fi performance. A recent kernel version often includes the latest Wi-Fi drivers and features." + + # Wi-Fi Interface INSIGHTS + log_and_display_plain "Wi-Fi Interface:" + if [ "$SSID" != "N/A" ]; then + log_and_display_plain "- Connected to network: $SSID" + + if [ "$(echo "$frequency2 < 2500" | bc -l)" -eq 1 ]; then + log_and_display_plain "- You are on the 2.4 GHz band. This has better range but might be more congested." + elif [ "$(echo "$frequency2 >= 5150 && $frequency2 <= 5875" | bc -l)" -eq 1 ]; then + log_and_display_plain "- You are on the 5 GHz band. This typically provides faster speeds but has shorter range." + elif [ "$(echo "$frequency2 >= 5925 && $frequency2 <= 7125" | bc -l)" -eq 1 ]; then + log_and_display_plain "- You are on the 6 GHz band. This typically provides even faster speeds but may have a shorter range." + else + log_and_display_plain "- Unknown frequency band. Characteristics are unknown." + fi + + signal_abs=$(echo "$signal_strength" | tr -d '-') + if [ "$(echo "$signal_abs < 50" | bc -l)" -eq 1 ]; then + log_and_display_plain "- Excellent signal strength. You should experience optimal performance." + elif [ "$(echo "$signal_abs < 60" | bc -l)" -eq 1 ]; then + log_and_display_plain "- Good signal strength. Should be sufficient for most applications." + elif [ "$(echo "$signal_abs < 70" | bc -l)" -eq 1 ]; then + log_and_display_plain "- Fair signal strength. You might experience some slowdowns." + else + log_and_display_plain "- Poor signal strength. Consider moving closer to your router or checking for obstacles." + fi + + tx_bitrate=$(echo "$link_info" | awk '/tx bitrate/{print $3}') + if [ "$(echo "$tx_bitrate > 100" | bc -l)" -eq 1 ]; then + log_and_display_plain "- Good transmission rate. Suitable for most high-bandwidth activities." + else + log_and_display_plain "- Lower transmission rate. You might experience slowdowns with high-bandwidth activities." + fi + else + log_and_display_plain "- This interface is not connected to any network." + fi + + # Speed Test INSIGHTS + log_and_display_plain "Speed Test:" + if [[ -n "$ping" && -n "$download" && -n "$upload" ]]; then + if [ "$(echo "$ping < 20" | bc -l)" -eq 1 ]; then + log_and_display_plain "- Excellent ping time. Great for real-time applications like gaming or video calls." + elif [ "$(echo "$ping < 50" | bc -l)" -eq 1 ]; then + log_and_display_plain "- Good ping time. Suitable for most online activities." + else + log_and_display_plain "- Higher ping time. You might experience lag in real-time applications." + fi + + if [ "$(echo "$download > 100" | bc -l)" -eq 1 ]; then + log_and_display_plain "- Fast download speed. Excellent for streaming, large file downloads, and multiple users. Note: Speed test results should be compared to other services such as Fast.com and Google Speed Test if the results feel wrong." + elif [ "$(echo "$download > 25" | bc -l)" -eq 1 ]; then + log_and_display_plain "- Good download speed. Suitable for most online activities and HD streaming. Note: Speed test results should be compared to other services such as Fast.com and Google Speed Test if the results feel wrong." + else + log_and_display_plain "- Lower download speed. You might experience buffering with HD streaming or slow file downloads. Note: Speed test results should be compared to other services such as Fast.com and Google Speed Test if the results feel wrong." + fi + + if [ "$(echo "$upload > 20" | bc -l)" -eq 1 ]; then + log_and_display_plain "- Good upload speed. Suitable for video calls, uploading large files, and online backups." + else + log_and_display_plain "- Lower upload speed. You might experience issues with video calls or uploading large files." + fi + else + log_and_display_plain "- Speed test results not available. Unable to provide insights on network performance." + fi + + # Network Environment INSIGHTS + log_and_display_plain "Network Environment:" + if [[ -n "$SIGNAL_LEVEL" && "$SIGNAL_LEVEL" =~ ^[0-9]+$ ]]; then + if [ "$SIGNAL_LEVEL" -lt "$QUALITY_THRESHOLD" ]; then + log_and_display_plain "- Signal quality is below ${QUALITY_THRESHOLD}%. This may impact your Wi-Fi performance." + else + log_and_display_plain "- Signal quality is good. This should provide stable Wi-Fi performance." + fi + fi + + # Connection Speed INSIGHTS + if [[ -n "$connection_speed" ]]; then + speed_value=$(echo "$connection_speed" | awk '{print int($1)}') + if (( speed_value < 20 )); then + log_and_display_plain "- Connection speed is below 20 Mbit/s. This may limit your ability to perform high-bandwidth activities." + else + log_and_display_plain "- Connection speed is good. This should support most online activities." + fi + fi + + # Power Save INSIGHTS + if [[ "$POWER_SAVE" == *"on"* ]]; then + log_and_display_plain "- Power save mode is on. This may improve battery life but could potentially impact Wi-Fi performance." + elif [[ "$POWER_SAVE" == *"off"* ]]; then + log_and_display_plain "- Power save mode is off. This may provide better Wi-Fi performance but could impact battery life on mobile devices." + else + log_and_display_plain "- Unable to determine power save mode. This information may not be available for your Wi-Fi interface." + fi + + log_and_display_plain + + # VPN INSIGHTS + log_and_display_plain "VPN Status:" + if [ "$VPN_ACTIVE" == "Yes" ]; then + log_and_display_plain "- A VPN ($VPN_TYPE) is active on interface $VPN_INTERFACE. This may impact your internet speed and latency, but provides increased privacy and security." + if [ "$VPN_TYPE" == "OpenVPN" ]; then + log_and_display_plain "- OpenVPN is known for its strong security features but may have slightly higher overhead compared to WireGuard." + elif [ "$VPN_TYPE" == "WireGuard" ]; then + log_and_display_plain "- WireGuard is known for its efficiency and speed, potentially offering better performance than OpenVPN." + fi + else + log_and_display_plain "- No VPN (tun0 OpenVPN or wg0 WireGuard) detected. Your internet traffic is not being routed through a VPN at the moment." + fi + + log_and_display_plain +} + +# Main function to run all checks +run_diagnostics() { + check_system_info + check_wifi_interfaces + perform_speed_test + if command -v nmcli >/dev/null 2>&1; then + check_nmcli_status + monitor_signal_quality + else + log_and_display_plain "nmcli not available. Skipping detailed Wi-Fi checks." + fi + summarize_findings +} + +# Script execution starts here +clear +print_title + +check_and_install_tools +log_and_display_plain + +log_and_display_plain "Starting Wi-Fi Diagnostics..." + +# Perform ping test and display results immediately after the script title +check_internet_connection + +run_diagnostics + +log_and_display_plain "Wi-Fi Diagnostics completed. For a full diagnostic report, check the log file at ${YELLOW}$LOGFILE${RESET}" diff --git a/Network-Diagnostic-Scripts/images/5gz1.png b/Network-Diagnostic-Scripts/images/5gz1.png new file mode 100644 index 0000000..e344a18 Binary files /dev/null and b/Network-Diagnostic-Scripts/images/5gz1.png differ diff --git a/Network-Diagnostic-Scripts/images/5gz2.png b/Network-Diagnostic-Scripts/images/5gz2.png new file mode 100644 index 0000000..f4a6b20 Binary files /dev/null and b/Network-Diagnostic-Scripts/images/5gz2.png differ diff --git a/Network-Diagnostic-Scripts/images/Ethernet-Diag.png b/Network-Diagnostic-Scripts/images/Ethernet-Diag.png new file mode 100644 index 0000000..30cabea Binary files /dev/null and b/Network-Diagnostic-Scripts/images/Ethernet-Diag.png differ diff --git a/Network-Diagnostic-Scripts/images/README b/Network-Diagnostic-Scripts/images/README new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/Network-Diagnostic-Scripts/images/README @@ -0,0 +1 @@ + diff --git a/Network-Diagnostic-Scripts/images/WiFi_Diag.png b/Network-Diagnostic-Scripts/images/WiFi_Diag.png new file mode 100644 index 0000000..2e661de Binary files /dev/null and b/Network-Diagnostic-Scripts/images/WiFi_Diag.png differ diff --git a/Tuned-PPD-Customizer-Script/LICENSE b/Tuned-PPD-Customizer-Script/LICENSE new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/Tuned-PPD-Customizer-Script/LICENSE @@ -0,0 +1 @@ + diff --git a/Tuned-PPD-Customizer-Script/images/1.png b/Tuned-PPD-Customizer-Script/images/1.png new file mode 100644 index 0000000..e9ba90b Binary files /dev/null and b/Tuned-PPD-Customizer-Script/images/1.png differ diff --git a/Tuned-PPD-Customizer-Script/images/2.png b/Tuned-PPD-Customizer-Script/images/2.png new file mode 100644 index 0000000..d21cb64 Binary files /dev/null and b/Tuned-PPD-Customizer-Script/images/2.png differ diff --git a/Tuned-PPD-Customizer-Script/images/3.png b/Tuned-PPD-Customizer-Script/images/3.png new file mode 100644 index 0000000..371e048 Binary files /dev/null and b/Tuned-PPD-Customizer-Script/images/3.png differ diff --git a/Tuned-PPD-Customizer-Script/images/4.png b/Tuned-PPD-Customizer-Script/images/4.png new file mode 100644 index 0000000..f7d7fa6 Binary files /dev/null and b/Tuned-PPD-Customizer-Script/images/4.png differ diff --git a/Tuned-PPD-Customizer-Script/images/5.png b/Tuned-PPD-Customizer-Script/images/5.png new file mode 100644 index 0000000..6451d55 Binary files /dev/null and b/Tuned-PPD-Customizer-Script/images/5.png differ diff --git a/Tuned-PPD-Customizer-Script/images/6.png b/Tuned-PPD-Customizer-Script/images/6.png new file mode 100644 index 0000000..5dcc40e Binary files /dev/null and b/Tuned-PPD-Customizer-Script/images/6.png differ diff --git a/Tuned-PPD-Customizer-Script/images/7.png b/Tuned-PPD-Customizer-Script/images/7.png new file mode 100644 index 0000000..a5544ff Binary files /dev/null and b/Tuned-PPD-Customizer-Script/images/7.png differ diff --git a/Tuned-PPD-Customizer-Script/images/readme b/Tuned-PPD-Customizer-Script/images/readme new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/Tuned-PPD-Customizer-Script/images/readme @@ -0,0 +1 @@ + diff --git a/Tuned-PPD-Customizer-Script/readme.md b/Tuned-PPD-Customizer-Script/readme.md new file mode 100644 index 0000000..15c1cc5 --- /dev/null +++ b/Tuned-PPD-Customizer-Script/readme.md @@ -0,0 +1,189 @@ +# Tuned-PPD Customizer Script Usage Guide + +### Important consideration +Out of the box, by default, Fedora 41 power settings are excellent. However, for those who want more control over their power performance, this script provides a helping hand while remaining with the existing GNOME power profile menu. + +## What Does This Script Do? + +This script helps you customize how **your Fedora install** manages power and performance through GNOME's power menu. It lets you change which power management profiles are used when you select "Power Saver" or "Performance" in the GNOME power menu. + +This script is part of the [Fedora Battery Life optimization page](https://knowledgebase.frame.work/en_us/optimizing-fedora-battery-life-r1baXZh). + + +## How It Works + +1. **GNOME Power Menu**: The menu in your system tray that lets you choose between power modes. +2. **The Configuration File** (`/etc/tuned/ppd.conf`): Stores your power profile settings. +3. **tuned-ppd.service**: Fedora's service that applies these settings when you change power modes. + +## What You Can Do With This Script + +- Back up your current power settings. +- Download additional power management profiles. +- Change which profile is used for "Power Saver" mode. +- Change which profile is used for "Performance" mode. +- View your current configuration. +- Restore previous settings from backup. + +**IMPORTANT:** Run options 1, 2 and then you can run option 4 to apply a new profile. Use [the guidance below](https://github.com/FrameworkComputer/linux-docs/tree/main/Tuned-PPD-Customizer-Script#recommendations) to select the right one for your powersaver and performance settings. + +![Launch screen](https://raw.githubusercontent.com/FrameworkComputer/linux-docs/refs/heads/main/Tuned-PPD-Customizer-Script/images/1.png) + +## Using the Script + +### Install Curl + +Curl should already be installed, but just in case: + +### Fedora +``` +sudo dnf install curl -y +``` +  +  +  + +## To Install Tuned-PPD Customizer Script, simply run: +``` +curl -s https://raw.githubusercontent.com/FrameworkComputer/linux-docs/refs/heads/main/Tuned-PPD-Customizer-Script/tuned-ppd.sh -o tuned-ppd.sh && clear && bash tuned-ppd.sh +``` +  +  +  + +1. Run the script after later on, from your home directory: + ```bash + bash tuned-ppd.sh + ``` + +2. Choose from the menu options to: + - Back up your settings. + - Apply a different profile. + - View current settings. + - Restore previous settings. + +## Available Profiles + +### Power Saving Profiles + +1. **balanced-battery** + - Balances performance and power consumption. + - Uses CPU frequency scaling to reduce power usage while maintaining reasonable performance. + +2. **cpu-partitioning-powersave** + - Optimizes power consumption for multi-core systems. + - Uses CPU frequency scaling and core parking. + +3. **desktop-powersave** + - Prioritizes power saving for desktop systems. + - Reduces CPU frequency, dims display, powers down idle devices. + +4. **laptop-ac-powersave** + - Balances performance and power for AC-powered laptops. + - Uses moderate power-saving techniques. + +5. **laptop-battery-powersave** + - Maximizes battery life. + - Uses aggressive power-saving measures. + +6. **powersave** + - Prioritizes power saving above all else. + - Uses maximum power-saving techniques. + +7. **server-powersave** + - Optimizes server power consumption. + - Balances server performance with energy efficiency. + +### Performance Profiles + +1. **accelerator-performance** + - Optimizes systems with hardware accelerators (GPUs/FPGAs). + - Prioritizes accelerator resource allocation. + +2. **enterprise-storage** + - Optimizes enterprise storage system performance. + - Tunes disk scheduling and I/O operations. + +3. **latency-performance** + - Minimizes system latency. + - Optimizes for low response times. + +4. **network-latency** + - Reduces network latency. + - Optimizes network settings for minimal delay. + +5. **network-throughput** + - Maximizes network throughput. + - Optimizes for high data transfer rates. + +6. **throughput-performance** + - Maximizes system throughput. + - Optimizes for high data processing rates. + +## Recommendations + +### For Laptops: Maximum Battery Life +Best profile: `laptop-battery-powersave` +- Optimized specifically for laptop battery operation. +- Aggressively reduces power consumption. +- Manages CPU frequency, screen brightness, and device power states. +- Best choice when you need to maximize battery life. + +### For Gaming: Maximum Performance +Best profile: `latency-performance` +- Minimizes system latency. +- Keeps CPU at maximum frequency. +- Optimizes for quick system response. +- Ideal for games where every millisecond counts. + +### For High Computational Tasks +Best profile: `accelerator-performance` +- Optimized for GPU and accelerator-heavy workloads, or just general high performance non-specific tasks. +- Ideal for video editing and machine learning tasks. +- Prioritizes accelerator and computational performance. +- Best choice when using CUDA, OpenCL, or similar GPU compute tasks. + +## Troubleshooting + +You are using tuned-ppd, not PPD or tuneD itself. Fedora 41 by default, is already using tuned-ppd. + +If you see the error `Error: /etc/tuned/ppd.conf does not exist!`: + +1. Verify the configuration file: + ```bash + cat /etc/tuned/ppd.conf + ``` +2. If missing, either: + - Create the file manually + - Reinstall the tuned package + +3. [Contact support before making additional changes if issues persist](https://framework.kustomer.help/contact/support-request-ryon9uAuq) + +## Important Notes and FAQ + +- The script is designed for Fedora installations, should work on any distro with tuned-ppd running though with some minor tweaks. +- We get the additional profiles from https://github.com/redhat-performance/tuned/ +- Changes take effect immediately after applying a new profile. +- Always keep a backup of your settings using this script. +- These recommendations are based on typical use cases; your specific needs may vary. +- The script clears the terminal screen between sections for better readability. +- Why are is this using bsdtar vs something else? It's been shown to be more reliable for this script. For Fedora users, it self-installs. +- **For enhanced control**, consider using the [TunedSwitcher](https://flathub.org/apps/org.easycoding.TunedSwitcher) Flatpak (compatible with GNOME/KDE). +- Ubuntu support is planned for future releases. + +## Screenshots + +![Launch screen](https://raw.githubusercontent.com/FrameworkComputer/linux-docs/refs/heads/main/Tuned-PPD-Customizer-Script/images/1.png) + +![Backup](https://raw.githubusercontent.com/FrameworkComputer/linux-docs/refs/heads/main/Tuned-PPD-Customizer-Script/images/2.png) + +![Install additional profiles](https://raw.githubusercontent.com/FrameworkComputer/linux-docs/refs/heads/main/Tuned-PPD-Customizer-Script/images/3.png) + +![Restore most recent backup](https://raw.githubusercontent.com/FrameworkComputer/linux-docs/refs/heads/main/Tuned-PPD-Customizer-Script/images/4.png) + +![Select a profile](https://raw.githubusercontent.com/FrameworkComputer/linux-docs/refs/heads/main/Tuned-PPD-Customizer-Script/images/5.png) + +![View powersave and performance configuration](https://raw.githubusercontent.com/FrameworkComputer/linux-docs/refs/heads/main/Tuned-PPD-Customizer-Script/images/6.png) + +(Note: my theme just happens to be purple, your default GNOME menu will be used) +![GNOME power menu remains default](https://raw.githubusercontent.com/FrameworkComputer/linux-docs/refs/heads/main/Tuned-PPD-Customizer-Script/images/7.png) diff --git a/Tuned-PPD-Customizer-Script/tuned-ppd.sh b/Tuned-PPD-Customizer-Script/tuned-ppd.sh new file mode 100644 index 0000000..6110d10 --- /dev/null +++ b/Tuned-PPD-Customizer-Script/tuned-ppd.sh @@ -0,0 +1,280 @@ +#!/usr/bin/env bash +clear + +# ANSI color codes +RED="\\e[31m" +GREEN="\\e[32m" +CYAN_BOLD="\\e[1;36m" +RESET="\\e[0m" + +# Directory for Tuned profiles +TUNED_PROFILES_DIR="/etc/tuned/profiles" + +# Configuration file for tuned-ppd +CONFIG_FILE="/etc/tuned/ppd.conf" + +# Validate configuration file existence +if [ ! -f "$CONFIG_FILE" ]; then + echo -e "${RED}Error:${RESET} $CONFIG_FILE does not exist!" + exit 1 +fi + +# Function to ensure bsdtar is installed +ensure_bsdtar() { + if ! command -v bsdtar &>/dev/null; then + echo -e "${CYAN_BOLD}bsdtar is not installed. Attempting to install...${RESET}" + + if [ -f /etc/os-release ]; then + . /etc/os-release # Source OS details + + if [[ "$ID" == "ubuntu" || "$ID" == "debian" ]]; then + echo -e "${CYAN_BOLD}Detected Ubuntu/Debian. Installing libarchive-tools (bsdtar)...${RESET}" + sudo apt update &>/dev/null && sudo apt install -y libarchive-tools &>/dev/null || { + echo -e "${RED}Failed to install bsdtar. Please install libarchive-tools manually.${RESET}" + exit 1 + } +elif [[ "$ID" == "fedora" ]]; then + echo -e "${CYAN_BOLD}Detected Fedora. Installing bsdtar...${RESET}" + sudo dnf install -y bsdtar &>/dev/null || { + echo -e "${RED}Failed to install bsdtar. Please install it manually.${RESET}" + exit 1 + } + else + echo -e "${RED}Unsupported distribution. Please install bsdtar manually.${RESET}" + exit 1 + fi + else + echo -e "${RED}Could not detect the operating system. Please install bsdtar manually.${RESET}" + exit 1 + fi + else + echo -e "${GREEN}bsdtar is already installed.${RESET}" + fi + +} + +# Describe the purpose of the script and explain tuned-ppd and GNOME's PPD menu +echo +echo -e "${CYAN_BOLD}This script helps you configure how your system manages power and performance.${RESET}" +echo +echo "Customize which tuned profiles correspond to menu options. Normally, this requires a third-party applet or similar, but with this script, you can configure it directly while continuing to use GNOME's power menu." | fmt -w 80 +echo + +# Function to back up the configuration file +backup_config() { + clear + BACKUP_FILE="/etc/tuned/ppd.conf.bak.$(date +%Y%m%d%H%M%S)" + + # Create a new backup + sudo cp "$CONFIG_FILE" "$BACKUP_FILE" && echo -e "${GREEN}Backup created: $BACKUP_FILE${RESET}" || { + echo -e "${RED}Failed to create backup.${RESET}" + exit 1 + } + + # Check for excess backups + BACKUPS=($(ls -t /etc/tuned/ppd.conf.bak.* 2>/dev/null)) + BACKUP_COUNT=${#BACKUPS[@]} + + if [[ $BACKUP_COUNT -gt 3 ]]; then + echo -e "${CYAN_BOLD}More than 3 backups detected. Removing oldest backups...${RESET}" + + # Remove oldest backups, keeping only the 3 newest + for ((i=3; i/dev/null | head -n 1) + + if [[ -z "$LATEST_BACKUP" ]]; then + echo -e "${RED}No backup file found to restore from.${RESET}" + return + fi + + # Restore the latest backup + sudo cp "$LATEST_BACKUP" "$CONFIG_FILE" && echo -e "${GREEN}Defaults restored from backup: $LATEST_BACKUP${RESET}" || { + echo -e "${RED}Failed to restore defaults.${RESET}" + } + echo -e "${CYAN_BOLD}Restarting tuned-ppd service...${RESET}" + sudo systemctl restart tuned-ppd +} + +# Function to apply a profile +apply_profile() { + clear + echo -e "${CYAN_BOLD}Which line do you want to update in [profiles]?${RESET}" + echo "" + echo "1) power-saver:" + echo " Selects a profile focused on reducing energy usage. Ideal for extending" + echo " battery life and keeping the system quieter and cooler. Typical tweaks" + echo " might lower CPU frequencies, reduce screen brightness, and apply other" + echo " measures that minimize power draw." + echo "" + echo "2) performance:" + echo " Selects a profile aimed at achieving maximum system speed and responsiveness." + echo " Perfect for demanding tasks like gaming, heavy computation, or large builds." + echo " This often means raising CPU frequencies, optimizing I/O operations, and" + echo " adjusting kernel parameters for improved throughput and lower latency." + echo "" + echo "q) Quit without making changes" + echo "" + + read -p "Enter choice [1-2 or q]: " CHOICE + case $CHOICE in + 1) + echo -e "${CYAN_BOLD}Available Profiles:${RESET}" + PROFILES=$(tuned-adm list | grep -E "powersave|battery" | grep -v -E "Current|active|profile:" | awk -F' - ' '{print $1}' | sed 's/^- //g' | sed '/^$/d') + if [ -z "$PROFILES" ]; then + echo -e "${RED}No profiles found for power-saver.${RESET}" + return + fi + select PROFILE_NAME in $PROFILES; do + if [[ -n "$PROFILE_NAME" ]]; then + sudo sed -i "s/^power-saver=.*/power-saver=$PROFILE_NAME/" "$CONFIG_FILE" + echo -e "${GREEN}Updated power-saver to use profile: $PROFILE_NAME${RESET}" + break + else + echo -e "${RED}Invalid selection. Please try again.${RESET}" + fi + done + ;; + 2) + echo -e "${CYAN_BOLD}Available Profiles:${RESET}" + PROFILES=$(tuned-adm list | grep -E "performance|throughput" | grep -v -E "Current|active|profile:" | awk -F' - ' '{print $1}' | sed 's/^- //g' | sed '/^$/d') + if [ -z "$PROFILES" ]; then + echo -e "${RED}No profiles found for performance.${RESET}" + return + fi + select PROFILE_NAME in $PROFILES; do + if [[ -n "$PROFILE_NAME" ]]; then + sudo sed -i "s/^performance=.*/performance=$PROFILE_NAME/" "$CONFIG_FILE" + echo -e "${GREEN}Updated performance to use profile: $PROFILE_NAME${RESET}" + break + else + echo -e "${RED}Invalid selection. Please try again.${RESET}" + fi + done + ;; + q) + echo -e "${CYAN_BOLD}No changes made.${RESET}" + return + ;; + *) + echo -e "${RED}Invalid choice. Please try again.${RESET}" + apply_profile + return + ;; + esac + + echo -e "${CYAN_BOLD}Restarting tuned-ppd service...${RESET}" + sudo systemctl restart tuned-ppd +} + +# Function to view specific section of /etc/tuned/ppd.conf +view_tuned_section() { + clear + local section=$1 + if [[ -f /etc/tuned/ppd.conf ]]; then + echo -e "\n### Displaying $section ###\n" + grep "^$section" /etc/tuned/ppd.conf | awk -F= '{print $2}' || echo "$section not found in /etc/tuned/ppd.conf" + echo -e "\n#########################################\n" + else + echo "Error: /etc/tuned/ppd.conf not found!" + fi +} + +# Function to view tuned configurations +view_tuned_configurations() { + clear + if [[ -f /etc/tuned/ppd.conf ]]; then + echo -e "${CYAN_BOLD}### Tuned Configurations ###${RESET}" + awk '/\[profiles\]/ {flag=1; next} /^\[/ {flag=0} flag {if($0 ~ /^(power-saver|performance)=/) print " " $0}' "$CONFIG_FILE" || echo "No relevant configurations found." + echo -e "${CYAN_BOLD}#########################################${RESET}" + else + echo -e "${CYAN_BOLD}Error: /etc/tuned/ppd.conf not found!${RESET}" + fi +} + +# Main menu function +main_menu() { + ensure_bsdtar + while true; do + echo -e "${CYAN_BOLD}Tuned-PPD Profile Manager${RESET}" + echo "1. Back Up Configuration" + echo "2. Download and Check for Missing Profiles" + echo "3. Restore from Backup" + echo "4. Apply a Profile" + echo "5. View Tuned Configurations" + echo "6. Exit" + + read -p "Choose an option: " OPTION + case $OPTION in + 1) backup_config ;; + 2) download_and_check_profiles ;; + 3) restore_defaults ;; + 4) apply_profile ;; + 5) view_tuned_configurations ;; + 6) clear && echo -e "${GREEN}Exiting.${RESET}" ; exit 0 ;; + *) echo -e "${RED}Invalid option. Please try again.${RESET}" ;; + esac + done +} + +# Start the main menu +main_menu diff --git a/Ubuntu22.04LTS-Manual-Setup-11thGen.md b/Ubuntu22.04LTS-Manual-Setup-11thGen.md index b45e498..1571891 100644 --- a/Ubuntu22.04LTS-Manual-Setup-11thGen.md +++ b/Ubuntu22.04LTS-Manual-Setup-11thGen.md @@ -1,109 +1,18 @@ # This is for 11th Gen Intel® Core™ Framework Laptop 13 ONLY. - -## This will: - -- Update your Ubuntu install's packages. -- Install the recommended OEM kernel and provide you with an alert should the OEM kernel needing updating. - - -        - - -### Step 1 - - Browse to Activities in the upper left corner, click to open it. - Type out the word terminal, click to open it. - Click on the small icon shown in the image below to copy the code below in the gray box, right click/paste it into the terminal window. - Then press the enter key, user password, enter key, **reboot.** ``` -sudo apt update && sudo apt upgrade -y && sudo snap refresh && sudo apt-get install linux-oem-22.04d -y +sudo apt update && sudo apt upgrade -y && sudo snap refresh ``` -> **TIP:** You can use the little clipboard icon to the right of the code to copy to your clipboard. - -

    Copy The Code Below Like This

    - - -      -### Step 2 - -- Browse to Activities in the upper left corner, click to open it. -- Type out the word terminal, click to open it. -- Click on the small icon shown in the image below to copy the code below in the gray box, right click/paste it into the terminal window. -- Then press the enter key, user password, enter key, **reboot.** - -``` -latest_oem_kernel=$(ls /boot/vmlinuz-* | grep '6.5.0-10..-oem' | sort -V | tail -n1 | awk -F'/' '{print $NF}' | sed 's/vmlinuz-//') && sudo sed -i.bak '/^GRUB_DEFAULT=/c\GRUB_DEFAULT="Advanced options for Ubuntu>Ubuntu, with Linux '"$latest_oem_kernel"'"' /etc/default/grub && sudo update-grub && sudo apt install zenity && mkdir -p ~/.config/autostart && [ ! -f ~/.config/autostart/kernel_check.desktop ] && echo -e "[Desktop Entry]\nType=Application\nExec=bash -c \"latest_oem_kernel=\$(ls /boot/vmlinuz-* | grep '6.5.0-10..-oem' | sort -V | tail -n1 | awk -F'/' '{print \\\$NF}' | sed 's/vmlinuz-//') && current_grub_kernel=\$(grep '^GRUB_DEFAULT=' /etc/default/grub | sed -e 's/GRUB_DEFAULT=\\\"Advanced options for Ubuntu>Ubuntu, with Linux //g' -e 's/\\\"//g') && [ \\\"\\\${latest_oem_kernel}\\\" != \\\"\\\${current_grub_kernel}\\\" ] && zenity --text-info --html --width=300 --height=200 --title=\\\"Kernel Update Notification\\\" --filename=<(echo -e \\\"A newer OEM D kernel is available than what is set in GRUB. Click here to learn more.\\\")\"\nHidden=false\nNoDisplay=false\nX-GNOME-Autostart-enabled=true\nName[en_US]=Kernel check\nName=Kernel check\nComment[en_US]=\nComment=" > ~/.config/autostart/kernel_check.desktop -``` > **TIP:** You can use the little clipboard icon to the right of the code to copy to your clipboard.

    Copy The Code Below Like This

    -        - - -## What the above code does. -- Ensures GRUB is using the latest OEM D kernel at every boot. -- Creates a desktop file as an autostart to check for OEM kernel status. -- If an update comes about for the OEM kernel, is installed, but GRUB still has the older version - an alert box will provide you with a link to get this corrected. - -        - -## What does the OEM Kernel alert looks like: -    -> **Note:** This will appear if the code below is pasted into the terminal, enter key pressed and system rebooted. -When a new version of the OEM kernel is ready, this will alert you at bootup - if you're *on the current OEM D kernel* AND you have *followed my above directions*, then and only then **you will not be alerted**. -![What does the OEM Kernel alert looks like](https://raw.githubusercontent.com/FrameworkComputer/linux-docs/d8becead412d3858a1f561fb2f827f803ab17c47/oem-d-alert.png) - -        - - ---------- - -## For Advanced users ONLY: - -> If you are someone who is not super comforable with the command line, **please use the steps above instead**. -> Additionally, if a new OEM kernel is released, **you will be NOT be alerted** if you use the advanced method as nothing is checking for updates to alert you. - -If you would rather enter the commands individually **instead** of using the code block provided previously: - - -### Step 1 (ADVANCED USERS) Updating packages. -``sudo apt update && sudo apt upgrade -y`` - -### Step 2 (ADVANCED USERS) Install the recommended OEM kernel. -``sudo apt install linux-oem-22.04c`` - -**Reboot** - -### Step 3 (ADVANCED USERS) Indentify your OEM D kernel. - -``` -ls /boot/vmlinuz-* | awk -F"-" '{split($0, a, "-"); version=a[3]; if (version>max) {max=version; kernel=a[2] "-" a[3] "-" a[4]}} END{print kernel}' -``` - -Right now, this is **6.5.0.1013-oem** - but this may evolve in the future. - - - -### Step 4 (ADVANCED USERS) Change the following. - - -`` -GRUB_DEFAULT="0" -`` - -into - -`` -GRUB_DEFAULT="Advanced options for Ubuntu>Ubuntu, with Linux 6.5.0.1013-oem" -`` - - -### Step 5 (ADVANCED USERS) Then run. -``sudo update-grub`` +      -**Reboot** diff --git a/Ubuntu22.04LTS-Manual-Setup-12thGen.md b/Ubuntu22.04LTS-Manual-Setup-12thGen.md index a9a43e3..4be0433 100644 --- a/Ubuntu22.04LTS-Manual-Setup-12thGen.md +++ b/Ubuntu22.04LTS-Manual-Setup-12thGen.md @@ -1,16 +1,7 @@ # This is for 12th Gen Intel® Core™ Framework Laptop 13 ONLY. -## This will: -- Update your Ubuntu install's packages. -- Install the recommended OEM kernel and provide you with an alert should the OEM kernel needing updating. -- Disable the ALS sensor so that your brightness keys work. - -        - - -### Step 1 - Browse to Activities in the upper left corner, click to open it. - Type out the word terminal, click to open it. @@ -18,97 +9,13 @@ - Then press the enter key, user password, enter key, **reboot.** ``` -sudo apt update && sudo apt upgrade -y && sudo snap refresh && sudo apt-get install linux-oem-22.04d -y -``` -> **TIP:** You can use the little clipboard icon to the right of the code to copy to your clipboard. - -

    Copy The Code Below Like This

    - -### Step 2 - -- Browse to Activities in the upper left corner, click to open it. -- Type out the word terminal, click to open it. -- Click on the small icon shown in the image below to copy the code below in the gray box, right click/paste it into the terminal window. -- Then press the enter key, user password, enter key, **reboot.** - - -``` -sudo sed -i 's/^GRUB_CMDLINE_LINUX_DEFAULT.*/GRUB_CMDLINE_LINUX_DEFAULT="quiet splash module_blacklist=hid_sensor_hub"/g' /etc/default/grub && latest_oem_kernel=$(ls /boot/vmlinuz-* | grep '6.5.0-10..-oem' | sort -V | tail -n1 | awk -F'/' '{print $NF}' | sed 's/vmlinuz-//') && sudo sed -i.bak '/^GRUB_DEFAULT=/c\GRUB_DEFAULT="Advanced options for Ubuntu>Ubuntu, with Linux '"$latest_oem_kernel"'"' /etc/default/grub && sudo update-grub && sudo apt install zenity && mkdir -p ~/.config/autostart && [ ! -f ~/.config/autostart/kernel_check.desktop ] && echo -e "[Desktop Entry]\nType=Application\nExec=bash -c \"latest_oem_kernel=\$(ls /boot/vmlinuz-* | grep '6.5.0-10..-oem' | sort -V | tail -n1 | awk -F'/' '{print \\\$NF}' | sed 's/vmlinuz-//') && current_grub_kernel=\$(grep '^GRUB_DEFAULT=' /etc/default/grub | sed -e 's/GRUB_DEFAULT=\\\"Advanced options for Ubuntu>Ubuntu, with Linux //g' -e 's/\\\"//g') && [ \\\"\\\${latest_oem_kernel}\\\" != \\\"\\\${current_grub_kernel}\\\" ] && zenity --text-info --html --width=300 --height=200 --title=\\\"Kernel Update Notification\\\" --filename=<(echo -e \\\"A newer OEM D kernel is available than what is set in GRUB. Click here to learn more.\\\")\"\nHidden=false\nNoDisplay=false\nX-GNOME-Autostart-enabled=true\nName[en_US]=Kernel check\nName=Kernel check\nComment[en_US]=\nComment=" > ~/.config/autostart/kernel_check.desktop +sudo apt update && sudo apt upgrade -y && sudo snap refresh ``` > **TIP:** You can use the little clipboard icon to the right of the code to copy to your clipboard.

    Copy The Code Below Like This

    -        -## What the above code does. -- Disables the ALS sensor so that your brightness keys work. -- Ensures GRUB is using the latest OEM D kernel at every boot. -- Creates a desktop file as an autostart to check for OEM kernel status. -- If an update comes about for the OEM kernel, is installed, but GRUB still has the older version - an alert box will provide you with a link to get this corrected. - -        - -## What does the OEM Kernel alert looks like: -    -> **Note:** This will appear if the code below is pasted into the terminal, enter key pressed and system rebooted. -When a new version of the OEM kernel is ready, this will alert you at bootup - if you're *on the current OEM D kernel* AND you have *followed my above directions*, then and only then **you will not be alerted**. - -![What does the OEM Kernel alert looks like](https://raw.githubusercontent.com/FrameworkComputer/linux-docs/d8becead412d3858a1f561fb2f827f803ab17c47/oem-d-alert.png) - -        - - ------ - -## For Advanced users ONLY: - -> If you are someone who is not super comforable with the command line, **please use the steps above instead**. -> Additionally, if a new OEM kernel is released, **you will be NOT be alerted** if you use the advanced method as nothing is checking for updates to alert you. - -If you would rather enter the commands individually **instead** of using the code block provided previously: - - -### Step 1 (ADVANCED USERS) Updating packages. -``sudo apt update && sudo apt upgrade -y`` - -### Step 2 (ADVANCED USERS) Install the recommended OEM kernel. -``sudo apt install linux-oem-22.04d`` - -**Reboot** - -### Step 3 (ADVANCED USERS) Disable the ALS sensor so that your brightness keys work. -``sudo gedit /etc/default/grub`` - -Add module_blacklist=hid_sensor_hub so it looks like: - -``GRUB_CMDLINE_LINUX_DEFAULT="quiet splash module_blacklist=hid_sensor_hub"`` - -### Step 4 (ADVANCED USERS) Indentify your OEM D kernel. - -``` -ls /boot/vmlinuz-* | awk -F"-" '{split($0, a, "-"); version=a[3]; if (version>max) {max=version; kernel=a[2] "-" a[3] "-" a[4]}} END{print kernel}' -``` - -Right now, this is **6.1.0-1025-oem** - but this may evolve in the future. - - - -### Step 5 (ADVANCED USERS) Change the following. - - -`` -GRUB_DEFAULT="0" -`` - -into - -`` -GRUB_DEFAULT="Advanced options for Ubuntu>Ubuntu, with Linux 6.1.0-1025-oem" -`` - -### Step 6 (ADVANCED USERS) Then run. -``sudo update-grub`` +      -**Reboot** diff --git a/Ubuntu22.04LTS-Manual-Setup-13thGen.md b/Ubuntu22.04LTS-Manual-Setup-13thGen.md index 9606767..df0d1af 100644 --- a/Ubuntu22.04LTS-Manual-Setup-13thGen.md +++ b/Ubuntu22.04LTS-Manual-Setup-13thGen.md @@ -1,15 +1,5 @@ # This is for 13th Gen Intel® Core™ Framework Laptop 13 ONLY. -## This will: - -- Update your Ubuntu install's packages. -- Install the recommended OEM kernel and provide you with an alert should the OEM kernel needing updating. -- Disable the ALS sensor so that your brightness keys work. - -        - - -### Step 1 - Browse to Activities in the upper left corner, click to open it. - Type out the word terminal, click to open it. @@ -17,7 +7,7 @@ - Then press the enter key, user password, enter key, **reboot.** ``` -sudo apt update && sudo apt upgrade -y && sudo snap refresh && sudo apt-get install linux-oem-22.04d -y +sudo apt update && sudo apt upgrade -y && sudo snap refresh ``` > **TIP:** You can use the little clipboard icon to the right of the code to copy to your clipboard. @@ -27,93 +17,3 @@ sudo apt update && sudo apt upgrade -y && sudo snap refresh && sudo apt-get inst       - -### Step 2 - -- Browse to Activities in the upper left corner, click to open it. -- Type out the word terminal, click to open it. -- Click on the small icon shown in the image below to copy the code below in the gray box, right click/paste it into the terminal window. -- Then press the enter key, user password, enter key, **reboot.** - - -``` -sudo sed -i 's/^GRUB_CMDLINE_LINUX_DEFAULT.*/GRUB_CMDLINE_LINUX_DEFAULT="quiet splash module_blacklist=hid_sensor_hub"/g' /etc/default/grub && latest_oem_kernel=$(ls /boot/vmlinuz-* | grep '6.5.0-10..-oem' | sort -V | tail -n1 | awk -F'/' '{print $NF}' | sed 's/vmlinuz-//') && sudo sed -i.bak '/^GRUB_DEFAULT=/c\GRUB_DEFAULT="Advanced options for Ubuntu>Ubuntu, with Linux '"$latest_oem_kernel"'"' /etc/default/grub && sudo update-grub && sudo apt install zenity && mkdir -p ~/.config/autostart && [ ! -f ~/.config/autostart/kernel_check.desktop ] && echo -e "[Desktop Entry]\nType=Application\nExec=bash -c \"latest_oem_kernel=\$(ls /boot/vmlinuz-* | grep '6.5.0-10..-oem' | sort -V | tail -n1 | awk -F'/' '{print \\\$NF}' | sed 's/vmlinuz-//') && current_grub_kernel=\$(grep '^GRUB_DEFAULT=' /etc/default/grub | sed -e 's/GRUB_DEFAULT=\\\"Advanced options for Ubuntu>Ubuntu, with Linux //g' -e 's/\\\"//g') && [ \\\"\\\${latest_oem_kernel}\\\" != \\\"\\\${current_grub_kernel}\\\" ] && zenity --text-info --html --width=300 --height=200 --title=\\\"Kernel Update Notification\\\" --filename=<(echo -e \\\"A newer OEM D kernel is available than what is set in GRUB. Click here to learn more.\\\")\"\nHidden=false\nNoDisplay=false\nX-GNOME-Autostart-enabled=true\nName[en_US]=Kernel check\nName=Kernel check\nComment[en_US]=\nComment=" > ~/.config/autostart/kernel_check.desktop -``` -> **TIP:** You can use the little clipboard icon to the right of the code to copy to your clipboard. - -

    Copy The Code Below Like This

    - -        - - -## What the above code does. -- Disables the ALS sensor so that your brightness keys work. -- Ensures GRUB is using the latest OEM D kernel at every boot. -- Creates a desktop file as an autostart to check for OEM kernel status. -- If an update comes about for the OEM kernel, is installed, but GRUB still has the older version - an alert box will provide you with a link to get this corrected. - -        - -## What does the OEM Kernel alert looks like: -    -> **Note:** This will appear if the code below is pasted into the terminal, enter key pressed and system rebooted. -When a new version of the OEM kernel is ready, this will alert you at bootup - if you're *on the current OEM D kernel* AND you have *followed my above directions*, then and only then **you will not be alerted**. - -![What does the OEM Kernel alert looks like](https://raw.githubusercontent.com/FrameworkComputer/linux-docs/d8becead412d3858a1f561fb2f827f803ab17c47/oem-d-alert.png) - -        - - ------ - -## For Advanced users ONLY: - -> If you are someone who is not super comforable with the command line, **please use the steps above instead**. -> Additionally, if a new OEM kernel is released, **you will be NOT be alerted** if you use the advanced method as nothing is checking for updates to alert you. - -If you would rather enter the commands individually **instead** of using the code block provided previously: - - -### Step 1 (ADVANCED USERS) Updating packages. -``sudo apt update && sudo apt upgrade -y`` - -### Step 2 (ADVANCED USERS) Install the recommended OEM kernel. -``sudo apt install linux-oem-22.04d`` - -**Reboot** - -### Step 3 (ADVANCED USERS) Disable the ALS sensor so that your brightness keys work. -``sudo gedit /etc/default/grub`` - -Add module_blacklist=hid_sensor_hub so it looks like: - -``GRUB_CMDLINE_LINUX_DEFAULT="quiet splash module_blacklist=hid_sensor_hub"`` - -### Step 4 (ADVANCED USERS) Indentify your OEM D kernel. - -``` -ls /boot/vmlinuz-* | awk -F"-" '{split($0, a, "-"); version=a[3]; if (version>max) {max=version; kernel=a[2] "-" a[3] "-" a[4]}} END{print kernel}' -``` - -Right now, this is **6.1.0-1025-oem** - but this may evolve in the future. - - - -### Step 5 (ADVANCED USERS) Change the following. - - -`` -GRUB_DEFAULT="0" -`` - -into - -`` -GRUB_DEFAULT="Advanced options for Ubuntu>Ubuntu, with Linux 6.1.0-1025-oem" -`` - - -### Step 6 (ADVANCED USERS) Then run. -``sudo update-grub`` - -**Reboot** diff --git a/Ubuntu24.04LTS-Setup-Intel-Core-Ultra-Series-1.md b/Ubuntu24.04LTS-Setup-Intel-Core-Ultra-Series-1.md index d9f6ca8..aca1f11 100644 --- a/Ubuntu24.04LTS-Setup-Intel-Core-Ultra-Series-1.md +++ b/Ubuntu24.04LTS-Setup-Intel-Core-Ultra-Series-1.md @@ -1,5 +1,9 @@ # This is for Intel® Core™ Ultra Series 1 Framework Laptop 13 ONLY. +#### Requires kernel 6.8.0-40 or better. Officially supporting from Ubuntu 24.04.1 +Please use the **"Get everything updated"** section below if you are on standard Ubuntu 24.04 without the the "dot 1 release." + +As of August 29th, the latest 24.04.1 ISOs are [live and avaialble](https://ubuntu.com/download/desktop). ## This will: diff --git a/Ubuntu24.04LTS-Setup-amd-fw16.md b/Ubuntu24.04LTS-Setup-amd-fw16.md index 928434c..d7d6566 100644 --- a/Ubuntu24.04LTS-Setup-amd-fw16.md +++ b/Ubuntu24.04LTS-Setup-amd-fw16.md @@ -5,7 +5,7 @@ - Update your Ubuntu install's packages. - (Optional) Stop buzzing sound from headphone jack if its present. -- We are NOT recommending an OEM kernel at this time, this may change in the future. Default kernel is where you need to be. +- Unlike in 22.04, we are NOT recommending an OEM kernel at this time, this may change in the future. Default kernel is where you need to be.         @@ -29,9 +29,11 @@ sudo apt update && sudo apt upgrade -y && sudo snap refresh       -### USB-C Video Out from dGPU directly +### (No Longer Needed) USB-C Video Out from dGPU directly +UPDATED: CURRENT FIRMWARE MAKES THIS UNNEEDED, JUST MAKE SURE [YOUR FIRMWARE IS CURRENT](https://guides.frame.work/Guide/Fedora+41+Installation+on+the+Framework+Laptop+16/394?lang=en#s2261). +**With the latest firmware, just connect your display**. -By default, when you attach a USB-C cable to the dGPU port, it will not come out of D3cold - this is by design and is to preserve your battery life during everyday usage. +By default, when you attach a USB-C cable to the dGPU port, it will not come out of [D3cold](https://learn.microsoft.com/en-us/windows-hardware/drivers/kernel/device-power-states) - this is by design and is to preserve your battery life during everyday usage. But you may find instances where you wish to connect to this port (HDMI/DP dongle to USB-C for example). There are a few ways to bring the dGPU out of D3cold. @@ -39,12 +41,12 @@ But you may find instances where you wish to connect to this port (HDMI/DP dongl - Installing nvtop, then using this method. ``` -sudo apt update && sudo apt install install nvtop -y +sudo apt update && sudo apt install nvtop -y ``` Create a script with the following: ``` -sudo /usr/local/bin/external_video.sh +sudo nano /usr/local/bin/external_video.sh ``` Paste in: @@ -56,7 +58,14 @@ timeout 2 nvtop echo "nvtop run completed." ``` -Save the file. Now setup a udev rule. + +Save the file. Then set it to executable. + +``` +sudo chmod +x /usr/local/bin/external_video.sh +``` + +Now setup a udev rule. ``` sudo nano /etc/udev/rules.d/99-external_video.rules ``` @@ -103,10 +112,10 @@ Then: ### Bonus Step (for former Mac users) Reduce Font Scaling to Match Your Needs -We received feedback that for users coming from OS X, installing GNOME Tweaks, browsing to Fonts, and reducing the font size from 1.00 to 0.80 may be preferred. +We received feedback that for users coming from macOS, that installing GNOME Tweaks, browsing to Fonts, and reducing the font size from 1.00 to 0.80 may be preferred. - Goto Displays, set scaling to 200%. This will look too large, so let's fix the fonts. -- Install with: +- Install GNOME Tweaks either by searching for it in Ubuntu Software or on the terminal with: ``` sudo apt update && sudo apt install gnome-tweaks -y @@ -117,3 +126,20 @@ sudo apt update && sudo apt install gnome-tweaks -y - At the top, select fonts. Now in that panel, scroll all the way down. Look for Size. Change from 1.00 to 0.80. Close Tweaks. Note: This is for the displays for the laptop only. This will look super odd on external displays and likely too large even still. + +  +  +  + +---------------------------------------- + +## Framework Laptop 16 not providing all of the expected refresh rates in kernels 6.9 and up - affects 24.10 Ubuntu at this time. +### + +[Framework Laptop 16 not providing all of the expected refresh rates ](https://github.com/FrameworkComputer/linux-docs/blob/main/amdgpu-workarounds/amdgpu_freesync_video/amdgpu_freesync_video.md#amdgpufreesync_video1-parameter-workaround-framework-laptop-16-only) + +  +  +   +  +  diff --git a/amdgpu-workarounds/amdgpu_freesync_video/Ubuntu_amdgpu.freesync_video_workaround.sh b/amdgpu-workarounds/amdgpu_freesync_video/Ubuntu_amdgpu.freesync_video_workaround.sh new file mode 100644 index 0000000..5e4c5c1 --- /dev/null +++ b/amdgpu-workarounds/amdgpu_freesync_video/Ubuntu_amdgpu.freesync_video_workaround.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +# Backup the current GRUB configuration +sudo cp /etc/default/grub /etc/default/grub.bak + +# Add the amdgpu.freesync_video=1 parameter if it's not already present +if grep -q "amdgpu.freesync_video=1" /etc/default/grub; then + echo "amdgpu.freesync_video=1 is already set in GRUB." +else + # Check if the GRUB_CMDLINE_LINUX_DEFAULT line exists, then append the parameter + sudo sed -i '/^GRUB_CMDLINE_LINUX_DEFAULT=/ s/"$/ amdgpu.freesync_video=1"/' /etc/default/grub + echo "amdgpu.freesync_video=1 has been added to GRUB." +fi + +# Update GRUB to apply changes +sudo update-grub + +echo "GRUB has been updated. Please reboot for changes to take effect." + + + +## There is a bug reported whereas not all of the refresh rates are being displayed in Display settings (GNOME or Plasma). + +The recommended workaround is to append amdgpu.freesync_video=1 to your grub settings. diff --git a/amdgpu-workarounds/amdgpu_freesync_video/amdgpu_freesync_video.md b/amdgpu-workarounds/amdgpu_freesync_video/amdgpu_freesync_video.md new file mode 100644 index 0000000..6860b68 --- /dev/null +++ b/amdgpu-workarounds/amdgpu_freesync_video/amdgpu_freesync_video.md @@ -0,0 +1,50 @@ +# NO Longer Needed - retaining just in case it resurfaces again +## amdgpu.freesync_video=1 parameter workaround Framework Laptop 16 ONLY +### For Framework Laptop 16 not providing all of the expected refresh rates in kernels 6.9 and up. + +Note: Once this is resolved, we'll keep this posted but mark it resolved. +Please ONLY run this if you were told to by support or, you meet the following criteria: + +- Ubuntu 24.10 or Fedora 40/41, kernels 6.9.x and HIGHER. +- Only run if you find the refresh rates that should be available are limited to 60 and 160. + +**Example Before:** + +![image](https://raw.githubusercontent.com/FrameworkComputer/linux-docs/refs/heads/main/amdgpu-workarounds/images/before.png) + +**Example After the workaround:** + +![image](https://raw.githubusercontent.com/FrameworkComputer/linux-docs/refs/heads/main/amdgpu-workarounds/images/after.png) + + + + +- **Ubuntu 24.10 Or for Ubuntu users on 6.9+ kernels - script grub workaround (most users)** +``` +curl -s https://raw.githubusercontent.com/FrameworkComputer/linux-docs/refs/heads/main/amdgpu-workarounds/amdgpu_freesync_video/Ubuntu_amdgpu.freesync_video_workaround.sh -o Ubuntu_amdgpu.freesync_video_workaround.sh && clear && bash Ubuntu_amdgpu.freesync_video_workaround.sh +``` + +- Copy and paste the above into a terminal. Press return, user password, then once complete, reboot. + +## Or if you prefer to do this manually on Ubuntu 24.10 + +**Ubuntu 24.10 manual method, no script for Ubuntu users on 6.9+ kernels - (advanced users)** + +``` +GRUB_CMDLINE_LINUX_DEFAULT="..........existing entries....amdgpu.freesync_video=1" +``` + +``` +sudo update-grub +``` + +- Run the above, reboot. + + +**Fedora 40/41 grub workaround** + +``` +sudo grubby --update-kernel=ALL --args="amdgpu.freesync_video=1" +``` + +- Copy and paste the above into a terminal. Press return, user password, then once complete, reboot. diff --git a/amdgpu-workarounds/images/after.png b/amdgpu-workarounds/images/after.png new file mode 100644 index 0000000..bc640d1 Binary files /dev/null and b/amdgpu-workarounds/images/after.png differ diff --git a/amdgpu-workarounds/images/before.png b/amdgpu-workarounds/images/before.png new file mode 100644 index 0000000..46562f5 Binary files /dev/null and b/amdgpu-workarounds/images/before.png differ diff --git a/amdgpu-workarounds/images/readme b/amdgpu-workarounds/images/readme new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/amdgpu-workarounds/images/readme @@ -0,0 +1 @@ + diff --git a/disable-accidental-wakeup/images/install.png b/disable-accidental-wakeup/images/install.png new file mode 100644 index 0000000..cd88712 Binary files /dev/null and b/disable-accidental-wakeup/images/install.png differ diff --git a/disable-accidental-wakeup/images/readme b/disable-accidental-wakeup/images/readme new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/disable-accidental-wakeup/images/readme @@ -0,0 +1 @@ + diff --git a/disable-accidental-wakeup/images/remove.png b/disable-accidental-wakeup/images/remove.png new file mode 100644 index 0000000..2dca2da Binary files /dev/null and b/disable-accidental-wakeup/images/remove.png differ diff --git a/disable-accidental-wakeup/readme.md b/disable-accidental-wakeup/readme.md new file mode 100644 index 0000000..e7c6e34 --- /dev/null +++ b/disable-accidental-wakeup/readme.md @@ -0,0 +1,83 @@ +# Disable Accidental Wakeup Script + +#### ATTENTION: Current BIOS should make this unneeded, please update your BIOS instead. + +Update your [BIOS instead](https://knowledgebase.frame.work/en_us/framework-laptop-16-bios-and-driver-releases-amd-ryzen-7040-series-BkeqkVovp). + +---------------------------------------------------- + +**Which laptop does this work with:** Framework Laptop 16. + +**(Considering this Beta/Testing as I am ironing out some keyboard backlighting behavior)** + +> +> +> **NOTE:** This may not disable the keyboard backlighting when you place it into suspend. By default without this script, the keyboard backlight goes out automatically. +With this script,you will need to **Fn space bar to turn off the backlight** before you enter suspend or it may remain on. This is a side effect of the script. +> +> + + + + + +**The problem:** In some instances, Framework Laptop 16 can accidentally come out of its suspend state. This usually occurs when traveling, walking, taking a bus, placing the laptop into a backpack. +Overall the agreed upon cause is that this happens due to keyboard presses while it's in a state of suspend, thus waking it up. + +**The workaround:** Our engineering team has it [on their roadmap](https://community.frame.work/t/responded-waking-from-suspend-w-lid-closed/47497/73?u=matt_hartley) to fix this on the BIOS level, however until that is available this script is a reliable workaround. + +**What this script does:** This script creates and enables a systemd service that prevents specific devices from waking the laptop from suspend. It disables wakeup functionality for **keyboard presses, touchpad presses, and lid lift events** by modifying the wakeup settings for USB devices and other relevant system devices. However, it ensures the system can still be brought out of suspend with a power button push. +The script configures the service to run at boot, ensuring these settings are applied consistently, and reloads the systemd daemon to recognize the new service. + +**How do I resume from suspend after running this script:** Press the power button one time. + +**Does this break functionality:** No, this script does not break functionality as long as it is implemented using this script on Ubuntu LTS or Fedora. +The script ensures that specific wakeup events, such as keyboard presses, touchpad presses, and lid lifts, are disabled, but it leaves the power button functional for resuming the system from suspend. + +- Suspend Behavior: The laptop can still enter suspend mode when the lid is closed. +- Resume Behavior: The laptop can be brought out of suspend using the power button. +- Disabled Wakeup Events: Keyboard and touchpad presses, as well as lifting the lid, will no longer wake the system, ensuring the system only resumes through intentional user interaction (e.g., the power button). + +**Restoring back to defaults:** Obviously this is not going to be a match for everyone, user habits may change. Therefore wwe also offer a script to restore your suspend configuration back to installation defaults. +This script is provided here as well. + +**Will this work on other distros:** Likely yes, I see no reason why it would not assuming paths and so forth match what we are doing here. But it is completely untested. + + +## Download and activate the Disable Accidental Wakeup Script + +Fedora, make sure curl is installed: + +``` +sudo dnf install curl -y +``` + +Ubuntu, make sure curl is installed: + +``` +sudo apt update && sudo apt install curl -y +``` + +Simply paste in this command into your kernel, press the enter: +(No reboot is needed, it's ready to go after running this script) + +``` +curl -s https://raw.githubusercontent.com/FrameworkComputer/linux-docs/main/disable-accidental-wakeup/wakeup.sh -o wakeup.sh && clear && sudo bash wakeup.sh +``` + +![Download the script](https://raw.githubusercontent.com/FrameworkComputer/linux-docs/refs/heads/main/disable-accidental-wakeup/images/install.png) + + + + +## Stop, disable and remove the Disable Accidental Wakeup Script +(Including the removal of disable-wakeup.service) +(No reboot is needed, it's ready to go after running this script) + +``` +curl -s https://raw.githubusercontent.com/FrameworkComputer/linux-docs/refs/heads/main/disable-accidental-wakeup/restore_defaults.sh -o restore_defaults.sh && clear && sudo bash restore_defaults.sh +``` + +![Removal script](https://raw.githubusercontent.com/FrameworkComputer/linux-docs/refs/heads/main/disable-accidental-wakeup/images/remove.png) + + diff --git a/disable-accidental-wakeup/restore_defaults.sh b/disable-accidental-wakeup/restore_defaults.sh new file mode 100644 index 0000000..6918970 --- /dev/null +++ b/disable-accidental-wakeup/restore_defaults.sh @@ -0,0 +1,66 @@ +#!/bin/bash + +# Script to restore wakeup functionality and remove custom disable-wakeup.service +# Compatible with Fedora and Ubuntu + +# Define the systemd service file path +SERVICE_FILE="/etc/systemd/system/disable-wakeup.service" + +# Step 1: Check for root privileges +if [[ $EUID -ne 0 ]]; then + echo "This script must be run as root. Please use sudo." + exit 1 +fi + +# Step 2: Stop and disable the service +echo "Stopping and disabling the disable-wakeup.service..." +systemctl stop disable-wakeup.service 2>/dev/null +systemctl disable disable-wakeup.service 2>/dev/null + +if [[ $? -eq 0 ]]; then + echo "Service stopped and disabled successfully." +else + echo "The service may not exist or was not running. Proceeding..." +fi + +# Step 3: Remove the systemd service file +if [[ -f "$SERVICE_FILE" ]]; then + echo "Removing the systemd service file at $SERVICE_FILE..." + rm -f "$SERVICE_FILE" + if [[ $? -eq 0 ]]; then + echo "Service file removed." + else + echo "Failed to remove the service file. Please check permissions." + exit 1 + fi +else + echo "Service file not found. Skipping removal." +fi + +# Step 4: Reload systemd daemon +echo "Reloading systemd daemon to apply changes..." +systemctl daemon-reload + +# Step 5: Restore default wakeup settings +echo "Restoring default wakeup settings for devices..." +for device in /sys/bus/usb/devices/*/power/wakeup; do + if [[ -f "$device" ]]; then + echo "enabled" > "$device" + echo "Restored default wakeup for $device" + fi +done + +if [[ -f "/sys/devices/platform/AMDI0010:03/i2c-1/i2c-PIXA3854:00/power/wakeup" ]]; then + echo "enabled" > /sys/devices/platform/AMDI0010:03/i2c-1/i2c-PIXA3854:00/power/wakeup + echo "Restored default wakeup for AMDI0010 device." +fi + +find /sys/devices -type f -name 'wakeup' | grep -i PNP0C0D | while read -r wakeup_file; do + echo "enabled" > "$wakeup_file" + echo "Restored default wakeup for $wakeup_file" +done + +# Final Step: Notify the user +echo "Restoration complete. All changes made by the disable-wakeup.service have been undone." +echo "If issues persist, verify the wakeup settings manually using: find /sys/devices -name 'wakeup'" + diff --git a/disable-accidental-wakeup/wakeup.sh b/disable-accidental-wakeup/wakeup.sh new file mode 100644 index 0000000..e39d925 --- /dev/null +++ b/disable-accidental-wakeup/wakeup.sh @@ -0,0 +1,62 @@ +#!/bin/bash + +# Script to create and enable a systemd service to disable wakeup for specific devices at boot +# Compatible with Fedora and Ubuntu + +# Define the systemd service file path +SERVICE_FILE="/etc/systemd/system/disable-wakeup.service" + +# Step 1: Check for root privileges +if [[ $EUID -ne 0 ]]; then + echo "This script must be run as root. Please use sudo." + exit 1 +fi + +# Step 2: Create the systemd service file +echo "Creating systemd service file at $SERVICE_FILE..." +cat << 'EOF' > "$SERVICE_FILE" +[Unit] +Description=Disable Wakeup on Devices +After=multi-user.target + +[Service] +Type=oneshot +ExecStart=/bin/bash -c "echo disabled > /sys/devices/platform/AMDI0010:03/i2c-1/i2c-PIXA3854:00/power/wakeup" +ExecStartPost=/bin/bash -c "for device in /sys/bus/usb/devices/*/power/wakeup; do echo disabled > \"$device\"; done" +ExecStartPost=/bin/bash -c "find /sys/devices -type f -name 'wakeup' | grep -i PNP0C0D | awk '{print \"echo disabled | sudo tee \" $0}' | bash" + +[Install] +WantedBy=multi-user.target +EOF + +echo "Systemd service file created." + +# Step 3: Reload systemd daemon to recognize the new service +echo "Reloading systemd daemon..." +systemctl daemon-reload +if [[ $? -ne 0 ]]; then + echo "Failed to reload systemd daemon. Exiting." + exit 1 +fi + +# Step 4: Enable the service to run at boot +echo "Enabling the service to run at boot..." +systemctl enable disable-wakeup.service +if [[ $? -ne 0 ]]; then + echo "Failed to enable the service. Exiting." + exit 1 +fi + +# Step 5: Optionally start the service immediately +echo "Starting the service to apply changes now..." +systemctl start disable-wakeup.service +if [[ $? -ne 0 ]]; then + echo "Failed to start the service. Check the service logs for details." + exit 1 +fi + +# Final Step: Notify the user +echo "Setup complete. The disable-wakeup.service is now active and will run at each boot." +echo "To verify the status of the service, use: sudo systemctl status disable-wakeup.service" +echo "To view logs, use: journalctl -u disable-wakeup.service" + diff --git a/dmidecode-and-CPU-info.md b/dmidecode-and-CPU-info.md index dc52339..e56c988 100644 --- a/dmidecode-and-CPU-info.md +++ b/dmidecode-and-CPU-info.md @@ -1,7 +1,7 @@ -# Fedora 37/38 only +# Fedora Only ## Copy and paste this into the terminal using your touchpad or mouse, then press enter. `` -sudo dnf install lshw dmidecode -y && sudo dmidecode | grep -A3 'Vendor:\|Product:' && sudo lshw -C cpu | grep -A3 'product:\|vendor:' +sudo dnf install lshw dmidecode -y && clear && sudo dmidecode | grep -A3 'Vendor:\|Product:' && sudo lshw -C cpu | grep -A3 'product:\|vendor:' `` diff --git a/easy-effects/Fedora-easy-effects-13-installer.sh b/easy-effects/Fedora-easy-effects-13-installer.sh new file mode 100644 index 0000000..0b195de --- /dev/null +++ b/easy-effects/Fedora-easy-effects-13-installer.sh @@ -0,0 +1,79 @@ +#!/bin/bash + +log_file="/tmp/easy_effects_install.log" + +# Function to install Easy Effects via Flatpak +install_easy_effects() { + echo "Installing Easy Effects via Flatpak..." | tee -a "$log_file" + + # Ensure Flatpak is installed + if ! command -v flatpak &> /dev/null; then + echo "Flatpak is not installed. Please install Flatpak first." | tee -a "$log_file" + exit 1 + fi + clear + + # Ensure Flathub is up-to-date + echo "Updating Flatpak appstream data..." | tee -a "$log_file" + sudo flatpak update --appstream -y | tee -a "$log_file" + + # Install Easy Effects + echo "Running Flatpak install command for Easy Effects..." | tee -a "$log_file" + flatpak install flathub com.github.wwmm.easyeffects -y | tee -a "$log_file" + if [ $? -ne 0 ]; then + echo "Flatpak installation failed. Please check the log for details." | tee -a "$log_file" + exit 1 + fi + clear + echo "Easy Effects installation completed." | tee -a "$log_file" +} + +# Install Easy Effects +install_easy_effects + +echo -e "Creating configuration directory...\n" | tee -a "$log_file" +clear + +# Define config directory and file +config_dir=~/.var/app/com.github.wwmm.easyeffects/config/easyeffects/output +config_file="$config_dir/fw13-easy-effects.json" +irs_dir=~/.var/app/com.github.wwmm.easyeffects/config/easyeffects/irs +irs_file="$irs_dir/IR_22ms_27dB_5t_15s_0c.irs" + +# Create config directory if it doesn't exist +mkdir -p "$config_dir" +mkdir -p "$irs_dir" + +echo -e "Downloading the configuration file...\n" | tee -a "$log_file" +clear +# Download the configuration file +curl -fo "$config_file" https://raw.githubusercontent.com/FrameworkComputer/linux-docs/main/easy-effects/fw13-easy-effects.json | tee -a "$log_file" + +# Check if the downloaded file is empty +if [ ! -s "$config_file" ]; then + echo -e "Error: The downloaded configuration file is empty. Please check the source URL.\n" | tee -a "$log_file" + exit 1 +fi + +echo -e "Configuration file downloaded to $config_file\n" | tee -a "$log_file" + +echo -e "Downloading the convolver impact file...\n" | tee -a "$log_file" +curl -fo "$irs_file" https://raw.githubusercontent.com/FrameworkComputer/linux-docs/main/easy-effects/irs/IR_22ms_27dB_5t_15s_0c.irs | tee -a "$log_file" +if [ ! -s "$irs_file" ]; then + echo -e "Error: The downloaded convolver impact file is empty. Please check the source URL.\n" | tee -a "$log_file" + exit 1 +fi +echo -e "Convolver impact file downloaded to $irs_file\n" | tee -a "$log_file" + +echo -e "Stopping any running Easy Effects processes...\n" | tee -a "$log_file" + +# Kill existing Easy Effects process if running +pkill easyeffects || true + +echo -e "Starting Easy Effects...\n" | tee -a "$log_file" +clear +# Start Easy Effects +nohup flatpak run com.github.wwmm.easyeffects &>/dev/null & + +echo -e "Easy Effects has been started.\n" | tee -a "$log_file" +echo -e "Please open Easy Effects and load the 'fw13-easy-effects' profile manually.\n" | tee -a "$log_file" diff --git a/easy-effects/Fedora-easy-effects-16-installer.sh b/easy-effects/Fedora-easy-effects-16-installer.sh new file mode 100644 index 0000000..b484b67 --- /dev/null +++ b/easy-effects/Fedora-easy-effects-16-installer.sh @@ -0,0 +1,74 @@ +#!/bin/bash + +log_file="/tmp/easy_effects_install.log" + +# Function to install Easy Effects via Flatpak +install_easy_effects() { + echo "Installing Easy Effects via Flatpak..." | tee -a "$log_file" + + # Ensure Flatpak is installed + if ! command -v flatpak &> /dev/null; then + echo "Flatpak is not installed. Please install Flatpak first." | tee -a "$log_file" + exit 1 + fi +clear + + # Ensure Flathub is up-to-date + echo "Updating Flatpak appstream data..." | tee -a "$log_file" + sudo flatpak update --appstream -y | tee -a "$log_file" + + # Install Easy Effects + echo "Running Flatpak install command for Easy Effects..." | tee -a "$log_file" + flatpak install flathub com.github.wwmm.easyeffects -y | tee -a "$log_file" + if [ $? -ne 0 ]; then + echo "Flatpak installation failed. Please check the log for details." | tee -a "$log_file" + exit 1 + fi +clear + echo "Easy Effects installation completed." | tee -a "$log_file" +} + +# Install Easy Effects +install_easy_effects + + +echo -e "Creating configuration directory...\n" | tee -a "$log_file" +clear + +# Define config directory and file +config_dir=~/.var/app/com.github.wwmm.easyeffects/config/easyeffects/output +config_file="$config_dir/fw16-easy-effects.json" + +# Create config directory if it doesn't exist +mkdir -p "$config_dir" + + +echo -e "Downloading the configuration file...\n" | tee -a "$log_file" +clear +# Download the configuration file +curl -o "$config_file" https://raw.githubusercontent.com/FrameworkComputer/linux-docs/main/easy-effects/fw16-easy-effects.json | tee -a "$log_file" + +# Check if the downloaded file is empty +if [ ! -s "$config_file" ]; then + echo -e "Error: The downloaded configuration file is empty. Please check the source URL.\n" | tee -a "$log_file" + exit 1 +fi + + +echo -e "Configuration file downloaded to $config_file\n" | tee -a "$log_file" + + +echo -e "Stopping any running Easy Effects processes...\n" | tee -a "$log_file" + +# Kill existing Easy Effects process if running +pkill easyeffects || true + + +echo -e "Starting Easy Effects...\n" | tee -a "$log_file" +clear +# Start Easy Effects +nohup flatpak run com.github.wwmm.easyeffects &>/dev/null & + + +echo -e "Easy Effects has been started.\n" | tee -a "$log_file" +echo -e "Please open Easy Effects and load the 'fw16-easy-effects' profile manually.\n" | tee -a "$log_file" diff --git a/easy-effects/README.md b/easy-effects/README.md new file mode 100644 index 0000000..773316b --- /dev/null +++ b/easy-effects/README.md @@ -0,0 +1,111 @@ +## Easy Effects for FW 16 and 13. + +### Sourced from [this Arch wiki guide](https://wiki.archlinux.org/title/Framework_Laptop_16#Easy_Effects). +#### fw16-easy-effects.json is based on [amesb's fw16 EE profile.json](https://gist.github.com/amesb/cc5d717472d7e322b5f551b643ff03f4) and fw13-easy-effects.json is based on [Gracefu's Edits.json](https://github.com/cab404/framework-dsp/blob/master/config/output/Gracefu's%20Edits.json). + +> It's worth noting, you can load these both up - even running one script after another is fine and will not overwrite anything. So if you want to compare fw13 vs fw16 scripts, you can. I find the fw16 option has more bass whereas the fw13 profile has more clarity. In other words, install both profiles with confidence that this is supported fully. It will attempt to install the flatpak twice, which is totally fine and won't change anything as it will sense the flatpak is already install and move on, installing the second sound profile. + +## For Fedora users on their Framework Laptop 16: + +### Automated method: + +Ensure curl is installed: + +``` +sudo dnf install curl -y +``` + +Then paste this and press enter. + +``` +curl https://raw.githubusercontent.com/FrameworkComputer/linux-docs/main/easy-effects/Fedora-easy-effects-16-installer.sh | bash +``` +\ +\ +\ +**IMPORTANT:** Load the profile by clicking the "Presets" pulldown, then "Load profile" option as shown below. + +![image](https://raw.githubusercontent.com/FrameworkComputer/linux-docs/main/easy-effects/images/fw16-easyeffects.png) + +----------------------- + +## For Fedora users on their Framework Laptop 13: + +### Automated method: + +Ensure curl is installed: + +``` +sudo dnf install curl -y +``` + +Then paste this and press enter. + +``` +curl https://raw.githubusercontent.com/FrameworkComputer/linux-docs/main/easy-effects/Fedora-easy-effects-13-installer.sh | bash +``` +\ +\ +\ +**IMPORTANT:** Load the profile by clicking the "Presets" pulldown, then "Load profile" option as shown below. + +![image](https://raw.githubusercontent.com/FrameworkComputer/linux-docs/main/easy-effects/images/fw16-easyeffects.png) + + +-------------------------- +## For Ubuntu users on their Framework Laptop 16: + +### Automated method: + +Ensure curl is installed: + +``` +sudo apt install curl -y +``` + +Then paste this and press enter. + +``` +curl https://raw.githubusercontent.com/FrameworkComputer/linux-docs/main/easy-effects/Ubuntu-easy-effects-16-installer.sh | bash +``` + +Then just load the profile by clicking the Load profile option as shown below. + +![image](https://raw.githubusercontent.com/FrameworkComputer/linux-docs/main/easy-effects/images/ubuntu-easy-effects.png) + +-------------------------- +## For Ubuntu users on their Framework Laptop 13: + +### Automated method: + +Ensure curl is installed: + +``` +sudo apt install curl -y +``` + +Then paste this and press enter. + +``` +curl https://raw.githubusercontent.com/FrameworkComputer/linux-docs/main/easy-effects/Ubuntu-easy-effects-13-installer.sh | bash +``` + +Then just load the profile by clicking the Load profile option as shown below. Yes, the image below shows FW16, but the image is merely a visual aid. It will reflect the appropriate profile. + +![image](https://raw.githubusercontent.com/FrameworkComputer/linux-docs/main/easy-effects/images/ubuntu-easy-effects.png) + +-------------------------- + +## FAQ + +- Can you run both of these to compare them? +> Yes, you can. Nothing is overwritten, it will just try to install the flatpack twice which is fine and affects nothing. + +- Do you need to look for the profile once it's installed? +> Nope, just follow the image. It's browsed for you, just load it. + +- I'd rather load this manually. How? +> We're going to recommend the automated method, but if you on our own, wish to do this: +> - Install Easy Effects. +> - Download the json file you wish to use. +> - Browse to it from the present menu. diff --git a/easy-effects/Ubuntu-easy-effects-13-installer.sh b/easy-effects/Ubuntu-easy-effects-13-installer.sh new file mode 100755 index 0000000..01d7f8d --- /dev/null +++ b/easy-effects/Ubuntu-easy-effects-13-installer.sh @@ -0,0 +1,73 @@ +#!/bin/bash + +log_file="/tmp/easy_effects_install.log" + +# Function to install Easy Effects via apt +install_easy_effects() { + echo "Installing Easy Effects via apt..." | tee -a "$log_file" + + # Update package list + echo "Updating package list..." | tee -a "$log_file" + sudo apt update | tee -a "$log_file" + + # Install Easy Effects + echo "Running apt install command for Easy Effects..." | tee -a "$log_file" + sudo apt install -y easyeffects | tee -a "$log_file" + if [ $? -ne 0 ]; then + echo "apt installation failed. Please check the log for details." | tee -a "$log_file" + exit 1 + fi + + echo "Easy Effects installation completed." | tee -a "$log_file" +} + +# Install Easy Effects +install_easy_effects + +echo -e "Creating configuration directory...\n" | tee -a "$log_file" + +# Define config directory and file +config_dir=~/.config/easyeffects/output +config_file="$config_dir/fw13-easy-effects.json" +irs_dir=~/.config/easyeffects/irs +irs_file="$irs_dir/IR_22ms_27dB_5t_15s_0c.irs" + +# Create config directory if it doesn't exist +mkdir -p "$config_dir" +mkdir -p "$irs_dir" + +echo -e "Downloading the configuration file...\n" | tee -a "$log_file" + +# Download the configuration file +curl -fo "$config_file" https://raw.githubusercontent.com/FrameworkComputer/linux-docs/main/easy-effects/fw13-easy-effects.json | tee -a "$log_file" + +# Check if the downloaded file is empty +if [ ! -s "$config_file" ]; then + echo -e "Error: The downloaded configuration file is empty. Please check the source URL.\n" | tee -a "$log_file" + exit 1 +fi +echo -e "Configuration file downloaded to $config_file\n" | tee -a "$log_file" + +echo -e "Downloading the convolver impact file...\n" | tee -a "$log_file" + +curl -fo "$irs_file" https://raw.githubusercontent.com/FrameworkComputer/linux-docs/main/easy-effects/irs/IR_22ms_27dB_5t_15s_0c.irs | tee -a "$log_file" +# Check if the downloaded file is empty +if [ ! -s "$irs_file" ]; then + echo -e "Error: The downloaded convolver file is empty. Please check the source URL.\n" | tee -a "$log_file" + exit 1 +fi + +echo -e "Convolver impact file downloaded to $irs_file\n" | tee -a "$log_file" + +echo -e "Stopping any running Easy Effects processes...\n" | tee -a "$log_file" + +# Kill existing Easy Effects process if running +pkill easyeffects || true + +echo -e "Starting Easy Effects...\n" | tee -a "$log_file" + +# Start Easy Effects +nohup easyeffects &>/dev/null & + +echo -e "Easy Effects has been started.\n" | tee -a "$log_file" +echo -e "Please open Easy Effects and load the 'fw13-easy-effects' profile manually.\n" | tee -a "$log_file" diff --git a/easy-effects/Ubuntu-easy-effects-16-installer.sh b/easy-effects/Ubuntu-easy-effects-16-installer.sh new file mode 100644 index 0000000..01a2b2e --- /dev/null +++ b/easy-effects/Ubuntu-easy-effects-16-installer.sh @@ -0,0 +1,60 @@ +#!/bin/bash + +log_file="/tmp/easy_effects_install.log" + +# Function to install Easy Effects via apt +install_easy_effects() { + echo "Installing Easy Effects via apt..." | tee -a "$log_file" + + # Update package list + echo "Updating package list..." | tee -a "$log_file" + sudo apt update | tee -a "$log_file" + + # Install Easy Effects + echo "Running apt install command for Easy Effects..." | tee -a "$log_file" + sudo apt install -y easyeffects | tee -a "$log_file" + if [ $? -ne 0 ]; then + echo "apt installation failed. Please check the log for details." | tee -a "$log_file" + exit 1 + fi + + echo "Easy Effects installation completed." | tee -a "$log_file" +} + +# Install Easy Effects +install_easy_effects + +echo -e "Creating configuration directory...\n" | tee -a "$log_file" + +# Define config directory and file +config_dir=~/.config/easyeffects/output +config_file="$config_dir/fw16-easy-effects.json" + +# Create config directory if it doesn't exist +mkdir -p "$config_dir" + +echo -e "Downloading the configuration file...\n" | tee -a "$log_file" + +# Download the configuration file +curl -o "$config_file" https://raw.githubusercontent.com/FrameworkComputer/linux-docs/main/easy-effects/fw16-easy-effects.json | tee -a "$log_file" + +# Check if the downloaded file is empty +if [ ! -s "$config_file" ]; then + echo -e "Error: The downloaded configuration file is empty. Please check the source URL.\n" | tee -a "$log_file" + exit 1 +fi + +echo -e "Configuration file downloaded to $config_file\n" | tee -a "$log_file" + +echo -e "Stopping any running Easy Effects processes...\n" | tee -a "$log_file" + +# Kill existing Easy Effects process if running +pkill easyeffects || true + +echo -e "Starting Easy Effects...\n" | tee -a "$log_file" + +# Start Easy Effects +nohup easyeffects &>/dev/null & + +echo -e "Easy Effects has been started.\n" | tee -a "$log_file" +echo -e "Please open Easy Effects and load the 'fw16-easy-effects' profile manually.\n" | tee -a "$log_file" diff --git a/easy-effects/fw13-easy-effects.json b/easy-effects/fw13-easy-effects.json new file mode 100644 index 0000000..d82595d --- /dev/null +++ b/easy-effects/fw13-easy-effects.json @@ -0,0 +1,319 @@ +{ + "output": { + "bass_enhancer#0": { + "amount": 4.0, + "blend": 0.0, + "bypass": true, + "floor": 10.0, + "floor-active": true, + "harmonics": 10.0, + "input-gain": 0.0, + "output-gain": 0.0, + "scope": 200.0 + }, + "blocklist": [], + "convolver#0": { + "autogain": true, + "bypass": false, + "input-gain": 0.0, + "ir-width": 100, + "kernel-name": "IR_22ms_27dB_5t_15s_0c", + "output-gain": 6.0 + }, + "filter#0": { + "balance": 0.0, + "bypass": false, + "equal-mode": "IIR", + "frequency": 60.0, + "gain": 0.0, + "input-gain": 0.0, + "mode": "RLC (BT)", + "output-gain": 0.0, + "quality": 16.0, + "slope": "x16", + "type": "High-pass", + "width": 1.0 + }, + "limiter#0": { + "alr": false, + "alr-attack": 5.0, + "alr-knee": 0.0, + "alr-release": 50.0, + "attack": 2.0, + "bypass": false, + "dithering": "None", + "external-sidechain": false, + "gain-boost": true, + "input-gain": 0.0, + "lookahead": 4.0, + "mode": "Herm Thin", + "output-gain": 0.0, + "oversampling": "Half x4(2L)", + "release": 8.0, + "sidechain-preamp": 0.0, + "stereo-link": 100.0, + "threshold": 0.0 + }, + "multiband_compressor#0": { + "band0": { + "attack-threshold": -16.0, + "attack-time": 150.0, + "boost-amount": 6.0, + "boost-threshold": -72.0, + "compression-mode": "Downward", + "compressor-enable": true, + "external-sidechain": false, + "knee": -12.0, + "makeup": 0.0, + "mute": false, + "ratio": 5.0, + "release-threshold": -100.0, + "release-time": 300.0, + "sidechain-custom-highcut-filter": false, + "sidechain-custom-lowcut-filter": false, + "sidechain-highcut-frequency": 500.0, + "sidechain-lookahead": 0.0, + "sidechain-lowcut-frequency": 10.0, + "sidechain-mode": "RMS", + "sidechain-preamp": 0.0, + "sidechain-reactivity": 10.0, + "sidechain-source": "Middle", + "solo": false, + "stereo-split-source": "Left/Right" + }, + "band1": { + "attack-threshold": -24.0, + "attack-time": 150.0, + "boost-amount": 6.0, + "boost-threshold": -72.0, + "compression-mode": "Downward", + "compressor-enable": true, + "enable-band": true, + "external-sidechain": false, + "knee": -9.0, + "makeup": 5.0, + "mute": false, + "ratio": 3.0, + "release-threshold": -100.0, + "release-time": 200.0, + "sidechain-custom-highcut-filter": false, + "sidechain-custom-lowcut-filter": false, + "sidechain-highcut-frequency": 1000.0, + "sidechain-lookahead": 0.0, + "sidechain-lowcut-frequency": 500.0, + "sidechain-mode": "RMS", + "sidechain-preamp": 0.0, + "sidechain-reactivity": 10.0, + "sidechain-source": "Middle", + "solo": false, + "split-frequency": 250.0, + "stereo-split-source": "Left/Right" + }, + "band2": { + "attack-threshold": -24.0, + "attack-time": 100.0, + "boost-amount": 6.0, + "boost-threshold": -72.0, + "compression-mode": "Downward", + "compressor-enable": true, + "enable-band": true, + "external-sidechain": false, + "knee": -9.0, + "makeup": 5.0, + "mute": false, + "ratio": 3.0, + "release-threshold": -100.0, + "release-time": 150.0, + "sidechain-custom-highcut-filter": false, + "sidechain-custom-lowcut-filter": false, + "sidechain-highcut-frequency": 2000.0, + "sidechain-lookahead": 0.0, + "sidechain-lowcut-frequency": 1000.0, + "sidechain-mode": "RMS", + "sidechain-preamp": 0.0, + "sidechain-reactivity": 10.0, + "sidechain-source": "Middle", + "solo": false, + "split-frequency": 1250.0, + "stereo-split-source": "Left/Right" + }, + "band3": { + "attack-threshold": -24.0, + "attack-time": 80.0, + "boost-amount": 6.0, + "boost-threshold": -72.0, + "compression-mode": "Downward", + "compressor-enable": true, + "enable-band": true, + "external-sidechain": false, + "knee": -9.0, + "makeup": 5.0, + "mute": false, + "ratio": 4.0, + "release-threshold": -100.0, + "release-time": 120.0, + "sidechain-custom-highcut-filter": false, + "sidechain-custom-lowcut-filter": false, + "sidechain-highcut-frequency": 4000.0, + "sidechain-lookahead": 0.0, + "sidechain-lowcut-frequency": 2000.0, + "sidechain-mode": "RMS", + "sidechain-preamp": 0.0, + "sidechain-reactivity": 10.0, + "sidechain-source": "Middle", + "solo": false, + "split-frequency": 5000.0, + "stereo-split-source": "Left/Right" + }, + "band4": { + "attack-threshold": -12.0, + "attack-time": 20.0, + "boost-amount": 6.0, + "boost-threshold": -72.0, + "compression-mode": "Downward", + "compressor-enable": true, + "enable-band": false, + "external-sidechain": false, + "knee": -6.0, + "makeup": 0.0, + "mute": false, + "ratio": 1.0, + "release-threshold": -100.0, + "release-time": 100.0, + "sidechain-custom-highcut-filter": false, + "sidechain-custom-lowcut-filter": false, + "sidechain-highcut-frequency": 8000.0, + "sidechain-lookahead": 0.0, + "sidechain-lowcut-frequency": 4000.0, + "sidechain-mode": "RMS", + "sidechain-preamp": 0.0, + "sidechain-reactivity": 10.0, + "sidechain-source": "Middle", + "solo": false, + "split-frequency": 4000.0, + "stereo-split-source": "Left/Right" + }, + "band5": { + "attack-threshold": -12.0, + "attack-time": 20.0, + "boost-amount": 6.0, + "boost-threshold": -72.0, + "compression-mode": "Downward", + "compressor-enable": true, + "enable-band": false, + "external-sidechain": false, + "knee": -6.0, + "makeup": 0.0, + "mute": false, + "ratio": 1.0, + "release-threshold": -100.0, + "release-time": 100.0, + "sidechain-custom-highcut-filter": false, + "sidechain-custom-lowcut-filter": false, + "sidechain-highcut-frequency": 12000.0, + "sidechain-lookahead": 0.0, + "sidechain-lowcut-frequency": 8000.0, + "sidechain-mode": "RMS", + "sidechain-preamp": 0.0, + "sidechain-reactivity": 10.0, + "sidechain-source": "Middle", + "solo": false, + "split-frequency": 8000.0, + "stereo-split-source": "Left/Right" + }, + "band6": { + "attack-threshold": -12.0, + "attack-time": 20.0, + "boost-amount": 6.0, + "boost-threshold": -72.0, + "compression-mode": "Downward", + "compressor-enable": true, + "enable-band": false, + "external-sidechain": false, + "knee": -6.0, + "makeup": 0.0, + "mute": false, + "ratio": 1.0, + "release-threshold": -100.0, + "release-time": 100.0, + "sidechain-custom-highcut-filter": false, + "sidechain-custom-lowcut-filter": false, + "sidechain-highcut-frequency": 16000.0, + "sidechain-lookahead": 0.0, + "sidechain-lowcut-frequency": 12000.0, + "sidechain-mode": "RMS", + "sidechain-preamp": 0.0, + "sidechain-reactivity": 10.0, + "sidechain-source": "Middle", + "solo": false, + "split-frequency": 12000.0, + "stereo-split-source": "Left/Right" + }, + "band7": { + "attack-threshold": -12.0, + "attack-time": 20.0, + "boost-amount": 6.0, + "boost-threshold": -72.0, + "compression-mode": "Downward", + "compressor-enable": true, + "enable-band": false, + "external-sidechain": false, + "knee": -6.0, + "makeup": 0.0, + "mute": false, + "ratio": 1.0, + "release-threshold": -100.0, + "release-time": 100.0, + "sidechain-custom-highcut-filter": false, + "sidechain-custom-lowcut-filter": false, + "sidechain-highcut-frequency": 20000.0, + "sidechain-lookahead": 0.0, + "sidechain-lowcut-frequency": 16000.0, + "sidechain-mode": "RMS", + "sidechain-preamp": 0.0, + "sidechain-reactivity": 10.0, + "sidechain-source": "Middle", + "solo": false, + "split-frequency": 16000.0, + "stereo-split-source": "Left/Right" + }, + "bypass": false, + "compressor-mode": "Modern", + "dry": -100.0, + "envelope-boost": "None", + "input-gain": 0.0, + "output-gain": 0.0, + "stereo-split": false, + "wet": 0.0 + }, + "plugins_order": [ + "filter#0", + "bass_enhancer#0", + "convolver#0", + "multiband_compressor#0", + "stereo_tools#0", + "limiter#0" + ], + "stereo_tools#0": { + "balance-in": 0.0, + "balance-out": 0.0, + "bypass": false, + "delay": 0.0, + "input-gain": 0.0, + "middle-level": 0.0, + "middle-panorama": 0.0, + "mode": "LR > LR (Stereo Default)", + "mutel": false, + "muter": false, + "output-gain": 0.0, + "phasel": false, + "phaser": false, + "sc-level": 1.0, + "side-balance": 0.0, + "side-level": 0.0, + "softclip": false, + "stereo-base": 0.30000000000000004, + "stereo-phase": 0.0 + } + } +} diff --git a/easy-effects/fw16-easy-effects.json b/easy-effects/fw16-easy-effects.json new file mode 100644 index 0000000..f9f614d --- /dev/null +++ b/easy-effects/fw16-easy-effects.json @@ -0,0 +1,310 @@ +{ + "output": { + "bass_enhancer#0": { + "amount": 7.999999999999986, + "blend": 0.0, + "bypass": false, + "floor": 10.0, + "floor-active": true, + "harmonics": 10.0, + "input-gain": 0.0, + "output-gain": 0.0, + "scope": 200.0 + }, + "blocklist": [], + "filter#1": { + "balance": 0.0, + "bypass": false, + "equal-mode": "IIR", + "frequency": 100.0, + "gain": 36.0, + "input-gain": 0.0, + "mode": "RLC (BT)", + "output-gain": 0.0, + "quality": 0.0, + "slope": "x1", + "type": "High-pass", + "width": 4.0 + }, + "limiter#0": { + "alr": false, + "alr-attack": 5.0, + "alr-knee": 0.0, + "alr-release": 50.0, + "attack": 2.0, + "bypass": false, + "dithering": "None", + "external-sidechain": false, + "gain-boost": true, + "input-gain": 0.0, + "lookahead": 4.0, + "mode": "Herm Thin", + "output-gain": 0.0, + "oversampling": "Half x4(2L)", + "release": 8.0, + "sidechain-preamp": 0.0, + "stereo-link": 100.0, + "threshold": 0.0 + }, + "multiband_compressor#0": { + "band0": { + "attack-threshold": -16.0, + "attack-time": 150.0, + "boost-amount": 6.0, + "boost-threshold": -72.0, + "compression-mode": "Downward", + "compressor-enable": true, + "external-sidechain": false, + "knee": -12.0, + "makeup": 4.999999999999997, + "mute": false, + "ratio": 5.0, + "release-threshold": -100.0, + "release-time": 300.0, + "sidechain-custom-highcut-filter": false, + "sidechain-custom-lowcut-filter": false, + "sidechain-highcut-frequency": 500.0, + "sidechain-lookahead": 0.0, + "sidechain-lowcut-frequency": 10.0, + "sidechain-mode": "RMS", + "sidechain-preamp": 0.0, + "sidechain-reactivity": 10.0, + "sidechain-source": "Middle", + "solo": false, + "stereo-split-source": "Left/Right" + }, + "band1": { + "attack-threshold": -24.0, + "attack-time": 150.0, + "boost-amount": 6.0, + "boost-threshold": -72.0, + "compression-mode": "Downward", + "compressor-enable": true, + "enable-band": true, + "external-sidechain": false, + "knee": -9.0, + "makeup": -1.942890293094024e-16, + "mute": false, + "ratio": 3.0, + "release-threshold": -100.0, + "release-time": 200.0, + "sidechain-custom-highcut-filter": false, + "sidechain-custom-lowcut-filter": false, + "sidechain-highcut-frequency": 1000.0, + "sidechain-lookahead": 0.0, + "sidechain-lowcut-frequency": 500.0, + "sidechain-mode": "RMS", + "sidechain-preamp": 0.0, + "sidechain-reactivity": 10.0, + "sidechain-source": "Middle", + "solo": false, + "split-frequency": 250.0, + "stereo-split-source": "Left/Right" + }, + "band2": { + "attack-threshold": -12.0, + "attack-time": 100.0, + "boost-amount": 6.0, + "boost-threshold": -72.0, + "compression-mode": "Downward", + "compressor-enable": true, + "enable-band": true, + "external-sidechain": false, + "knee": -9.0, + "makeup": 1.4999999999999987, + "mute": false, + "ratio": 3.0, + "release-threshold": -100.0, + "release-time": 150.0, + "sidechain-custom-highcut-filter": false, + "sidechain-custom-lowcut-filter": false, + "sidechain-highcut-frequency": 2000.0, + "sidechain-lookahead": 0.0, + "sidechain-lowcut-frequency": 1000.0, + "sidechain-mode": "RMS", + "sidechain-preamp": 0.0, + "sidechain-reactivity": 10.0, + "sidechain-source": "Middle", + "solo": false, + "split-frequency": 1250.0, + "stereo-split-source": "Left/Right" + }, + "band3": { + "attack-threshold": -24.0, + "attack-time": 80.0, + "boost-amount": 6.0, + "boost-threshold": -72.0, + "compression-mode": "Downward", + "compressor-enable": true, + "enable-band": true, + "external-sidechain": false, + "knee": -9.0, + "makeup": 4.9999999999999964, + "mute": false, + "ratio": 4.0, + "release-threshold": -100.0, + "release-time": 120.0, + "sidechain-custom-highcut-filter": false, + "sidechain-custom-lowcut-filter": false, + "sidechain-highcut-frequency": 4000.0, + "sidechain-lookahead": 0.0, + "sidechain-lowcut-frequency": 2000.0, + "sidechain-mode": "RMS", + "sidechain-preamp": 0.0, + "sidechain-reactivity": 10.0, + "sidechain-source": "Middle", + "solo": false, + "split-frequency": 5000.0, + "stereo-split-source": "Left/Right" + }, + "band4": { + "attack-threshold": -12.0, + "attack-time": 20.0, + "boost-amount": 6.0, + "boost-threshold": -72.0, + "compression-mode": "Downward", + "compressor-enable": true, + "enable-band": false, + "external-sidechain": false, + "knee": -6.0, + "makeup": 0.0, + "mute": false, + "ratio": 1.0, + "release-threshold": -100.0, + "release-time": 100.0, + "sidechain-custom-highcut-filter": false, + "sidechain-custom-lowcut-filter": false, + "sidechain-highcut-frequency": 8000.0, + "sidechain-lookahead": 0.0, + "sidechain-lowcut-frequency": 4000.0, + "sidechain-mode": "RMS", + "sidechain-preamp": 0.0, + "sidechain-reactivity": 10.0, + "sidechain-source": "Middle", + "solo": false, + "split-frequency": 4000.0, + "stereo-split-source": "Left/Right" + }, + "band5": { + "attack-threshold": -12.0, + "attack-time": 20.0, + "boost-amount": 6.0, + "boost-threshold": -72.0, + "compression-mode": "Downward", + "compressor-enable": true, + "enable-band": false, + "external-sidechain": false, + "knee": -6.0, + "makeup": 0.0, + "mute": false, + "ratio": 1.0, + "release-threshold": -100.0, + "release-time": 100.0, + "sidechain-custom-highcut-filter": false, + "sidechain-custom-lowcut-filter": false, + "sidechain-highcut-frequency": 12000.0, + "sidechain-lookahead": 0.0, + "sidechain-lowcut-frequency": 8000.0, + "sidechain-mode": "RMS", + "sidechain-preamp": 0.0, + "sidechain-reactivity": 10.0, + "sidechain-source": "Middle", + "solo": false, + "split-frequency": 8000.0, + "stereo-split-source": "Left/Right" + }, + "band6": { + "attack-threshold": -12.0, + "attack-time": 20.0, + "boost-amount": 6.0, + "boost-threshold": -72.0, + "compression-mode": "Downward", + "compressor-enable": true, + "enable-band": false, + "external-sidechain": false, + "knee": -6.0, + "makeup": 0.0, + "mute": false, + "ratio": 1.0, + "release-threshold": -100.0, + "release-time": 100.0, + "sidechain-custom-highcut-filter": false, + "sidechain-custom-lowcut-filter": false, + "sidechain-highcut-frequency": 16000.0, + "sidechain-lookahead": 0.0, + "sidechain-lowcut-frequency": 12000.0, + "sidechain-mode": "RMS", + "sidechain-preamp": 0.0, + "sidechain-reactivity": 10.0, + "sidechain-source": "Middle", + "solo": false, + "split-frequency": 12000.0, + "stereo-split-source": "Left/Right" + }, + "band7": { + "attack-threshold": -12.0, + "attack-time": 20.0, + "boost-amount": 6.0, + "boost-threshold": -72.0, + "compression-mode": "Downward", + "compressor-enable": true, + "enable-band": false, + "external-sidechain": false, + "knee": -6.0, + "makeup": 0.0, + "mute": false, + "ratio": 1.0, + "release-threshold": -100.0, + "release-time": 100.0, + "sidechain-custom-highcut-filter": false, + "sidechain-custom-lowcut-filter": false, + "sidechain-highcut-frequency": 20000.0, + "sidechain-lookahead": 0.0, + "sidechain-lowcut-frequency": 16000.0, + "sidechain-mode": "RMS", + "sidechain-preamp": 0.0, + "sidechain-reactivity": 10.0, + "sidechain-source": "Middle", + "solo": false, + "split-frequency": 16000.0, + "stereo-split-source": "Left/Right" + }, + "bypass": false, + "compressor-mode": "Modern", + "dry": -100.0, + "envelope-boost": "None", + "input-gain": -3.0, + "output-gain": 0.0, + "stereo-split": false, + "wet": 0.0 + }, + "plugins_order": [ + "filter#1", + "bass_enhancer#0", + "multiband_compressor#0", + "stereo_tools#0", + "limiter#0" + ], + "stereo_tools#0": { + "balance-in": 0.0, + "balance-out": 0.0, + "bypass": false, + "delay": 0.0, + "input-gain": 0.0, + "middle-level": 0.0, + "middle-panorama": 0.0, + "mode": "LR > LR (Stereo Default)", + "mutel": false, + "muter": false, + "output-gain": 0.0, + "phasel": false, + "phaser": false, + "sc-level": 1.0, + "side-balance": 0.0, + "side-level": 0.0, + "softclip": false, + "stereo-base": 0.1499999999999999, + "stereo-phase": 0.0 + } + } +} diff --git a/easy-effects/images/fw16-easyeffects.png b/easy-effects/images/fw16-easyeffects.png new file mode 100644 index 0000000..1b00433 Binary files /dev/null and b/easy-effects/images/fw16-easyeffects.png differ diff --git a/easy-effects/images/readme b/easy-effects/images/readme new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/easy-effects/images/readme @@ -0,0 +1 @@ + diff --git a/easy-effects/images/ubuntu-easy-effects.png b/easy-effects/images/ubuntu-easy-effects.png new file mode 100644 index 0000000..cdfaf2d Binary files /dev/null and b/easy-effects/images/ubuntu-easy-effects.png differ diff --git a/easy-effects/irs/IR_22ms_27dB_5t_15s_0c.irs b/easy-effects/irs/IR_22ms_27dB_5t_15s_0c.irs new file mode 100644 index 0000000..2124f64 Binary files /dev/null and b/easy-effects/irs/IR_22ms_27dB_5t_15s_0c.irs differ diff --git a/flatpaks/flatseal-installer.sh b/flatpaks/flatseal-installer.sh new file mode 100644 index 0000000..ad3a7b7 --- /dev/null +++ b/flatpaks/flatseal-installer.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +# Update and install Flatpak +sudo apt update +sudo apt install -y flatpak + +# Add the Flathub repository (if not already added) +sudo flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo + +# Install Flatseal Flathub +flatpak install flathub com.github.tchx84.Flatseal -y + +# Verify the installation +flatpak list | grep Flatseal + +echo "Flatseal been installed successfully." diff --git a/flatpaks/images/flatseal-gimp-filesystem.png b/flatpaks/images/flatseal-gimp-filesystem.png new file mode 100644 index 0000000..2e7463b Binary files /dev/null and b/flatpaks/images/flatseal-gimp-filesystem.png differ diff --git a/flatpaks/images/flatseal.png b/flatpaks/images/flatseal.png new file mode 100644 index 0000000..de2bc8b Binary files /dev/null and b/flatpaks/images/flatseal.png differ diff --git a/flatpaks/images/mission.png b/flatpaks/images/mission.png new file mode 100644 index 0000000..439701b Binary files /dev/null and b/flatpaks/images/mission.png differ diff --git a/flatpaks/images/readme b/flatpaks/images/readme new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/flatpaks/images/readme @@ -0,0 +1 @@ + diff --git a/flatpaks/mission-center-installer.sh b/flatpaks/mission-center-installer.sh new file mode 100644 index 0000000..c781108 --- /dev/null +++ b/flatpaks/mission-center-installer.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +# Update and install Flatpak +sudo apt update +sudo apt install -y flatpak + +# Add the Flathub repository (if not already added) +sudo flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo + +# Install Mission Center from Flathub +flatpak install flathub io.missioncenter.MissionCenter -y + +# Verify the installation +flatpak list | grep MissionCenter + +echo "Mission Center has been installed successfully." diff --git a/flatpaks/readme.md b/flatpaks/readme.md new file mode 100644 index 0000000..0e97e05 --- /dev/null +++ b/flatpaks/readme.md @@ -0,0 +1,26 @@ +# Flatpaks, what are they? + +By design, Flatpaks have limited access to your home folder and system in general. For most applications, this is perfectly fine, though in some cases this may limit the access you need—such as a webcam or microphone for Zoom, or a directory outside your home folder (for example, an external flash/thumb drive). You can extend this access using Flatseal, which itself can be installed via Flatpak. + +## Setting up Flatseal on Ubuntu + +- Step 1 + +``` +sudo apt install curl -y && \ +curl -O https://raw.githubusercontent.com/FrameworkComputer/linux-docs/main/flatpaks/flatseal-installer.sh && \ +bash flatseal-installer.sh +``` + +## Setting up Mission Center Installer for Ubuntu +- Step 1 + +``` +sudo apt install curl -y && \ +curl -O https://raw.githubusercontent.com/FrameworkComputer/linux-docs/main/flatpaks/mission-center-installer.sh && \ +bash mission-center-installer.sh +``` + +- Step 2 + + Log out, then log back in or reboot. diff --git a/framework-desktop/Fedora-42.md b/framework-desktop/Fedora-42.md new file mode 100644 index 0000000..18ac00d --- /dev/null +++ b/framework-desktop/Fedora-42.md @@ -0,0 +1,63 @@ +# This is for Framework Desktop ONLY + +## This will: + +- Getting your desktop fully updated. +- Enable improved fractional scaling support Fedora's GNOME environment using Wayland. + +  +  +  + + +### Step 1 Updating your software packages + +- Browse to the horizontal line in the upper left corner, click to open it. +- Type out the word terminal, click to open it. +- Copy the code below in the gray box, right click/paste it into the terminal window. +- Then press the enter key, user password, enter key, **reboot.** + + +``` +sudo dnf upgrade +``` +> **TIP:** You can use the little clipboard icon to the right of the code to copy to your clipboard. + + +**Reboot** + +  +  +  + +### Step 2 - If you want to enable fractional scaling on Wayland: + +- Type out the word Displays. +- Look for scale you want and select it, click Apply. + +  +  +  + + +### Bonus Step (for former Mac users) Reduce Font Scaling to Match Your Needs + +We received feedback that for users coming from OS X, installing GNOME Tweaks, browsing to Fonts, and reducing the font size from 1.00 to 0.80 may be preferred. Your own display may vary, so note any changes made if you need to revert back. + +- Goto Displays, set scaling to 200%. This will look too large, so let's fix the fonts. + +- Install with: + +``` +sudo dnf install gnome-tweaks -y +``` + +- Open Tweaks by using the "Super" or Windows key, search tweaks, and enter. + +- At the top, select fonts. Now in that panel, scroll all the way down. Look for Size. Change from 1.00 to 0.80. Close Tweaks. This will vary depending on what you are using for fractional scaling under Displays. + + + +  +  +  diff --git a/framework-desktop/Fedora-all.md b/framework-desktop/Fedora-all.md new file mode 100644 index 0000000..18ac00d --- /dev/null +++ b/framework-desktop/Fedora-all.md @@ -0,0 +1,63 @@ +# This is for Framework Desktop ONLY + +## This will: + +- Getting your desktop fully updated. +- Enable improved fractional scaling support Fedora's GNOME environment using Wayland. + +  +  +  + + +### Step 1 Updating your software packages + +- Browse to the horizontal line in the upper left corner, click to open it. +- Type out the word terminal, click to open it. +- Copy the code below in the gray box, right click/paste it into the terminal window. +- Then press the enter key, user password, enter key, **reboot.** + + +``` +sudo dnf upgrade +``` +> **TIP:** You can use the little clipboard icon to the right of the code to copy to your clipboard. + + +**Reboot** + +  +  +  + +### Step 2 - If you want to enable fractional scaling on Wayland: + +- Type out the word Displays. +- Look for scale you want and select it, click Apply. + +  +  +  + + +### Bonus Step (for former Mac users) Reduce Font Scaling to Match Your Needs + +We received feedback that for users coming from OS X, installing GNOME Tweaks, browsing to Fonts, and reducing the font size from 1.00 to 0.80 may be preferred. Your own display may vary, so note any changes made if you need to revert back. + +- Goto Displays, set scaling to 200%. This will look too large, so let's fix the fonts. + +- Install with: + +``` +sudo dnf install gnome-tweaks -y +``` + +- Open Tweaks by using the "Super" or Windows key, search tweaks, and enter. + +- At the top, select fonts. Now in that panel, scroll all the way down. Look for Size. Change from 1.00 to 0.80. Close Tweaks. This will vary depending on what you are using for fractional scaling under Displays. + + + +  +  +  diff --git a/framework-desktop/Ubuntu-25.10.md b/framework-desktop/Ubuntu-25.10.md new file mode 100644 index 0000000..24c7f08 --- /dev/null +++ b/framework-desktop/Ubuntu-25.10.md @@ -0,0 +1,63 @@ +# This is for Framework Desktop ONLY + +## This will: + +- Getting your desktop fully updated. +- Enable improved fractional scaling support Ubuntu's GNOME environment using Wayland. + +  +  +  + + +### Step 1 Updating your software packages + +- Browse to the horizontal line in the upper left corner, click to open it. +- Type out the word terminal, click to open it. +- Copy the code below in the gray box, right click/paste it into the terminal window. +- Then press the enter key, user password, enter key, **reboot.** + + +``` +sudo apt update && sudo apt upgrade -y && sudo snap refresh +``` +> **TIP:** You can use the little clipboard icon to the right of the code to copy to your clipboard. + + +**Reboot** + +  +  +  + +### Step 2 - If you want to enable fractional scaling on Wayland: + +- Type out the word Displays. +- Look for scale you want and select it, click Apply. + +  +  +  + + +### Bonus Step (for former Mac users) Reduce Font Scaling to Match Your Needs + +We received feedback that for users coming from OS X, installing GNOME Tweaks, browsing to Fonts, and reducing the font size from 1.00 to 0.80 may be preferred. Your own display may vary, so note any changes made if you need to revert back. + +- Goto Displays, set scaling to 200%. This will look too large, so let's fix the fonts. + +- Install with: + +``` +sudo dnf install gnome-tweaks -y +``` + +- Open Tweaks by using the "Super" or Windows key, search tweaks, and enter. + +- At the top, select fonts. Now in that panel, scroll all the way down. Look for Size. Change from 1.00 to 0.80. Close Tweaks. This will vary depending on what you are using for fractional scaling under Displays. + + + +  +  +  diff --git a/framework-desktop/Ubuntu-26.04.md b/framework-desktop/Ubuntu-26.04.md new file mode 100644 index 0000000..24c7f08 --- /dev/null +++ b/framework-desktop/Ubuntu-26.04.md @@ -0,0 +1,63 @@ +# This is for Framework Desktop ONLY + +## This will: + +- Getting your desktop fully updated. +- Enable improved fractional scaling support Ubuntu's GNOME environment using Wayland. + +  +  +  + + +### Step 1 Updating your software packages + +- Browse to the horizontal line in the upper left corner, click to open it. +- Type out the word terminal, click to open it. +- Copy the code below in the gray box, right click/paste it into the terminal window. +- Then press the enter key, user password, enter key, **reboot.** + + +``` +sudo apt update && sudo apt upgrade -y && sudo snap refresh +``` +> **TIP:** You can use the little clipboard icon to the right of the code to copy to your clipboard. + + +**Reboot** + +  +  +  + +### Step 2 - If you want to enable fractional scaling on Wayland: + +- Type out the word Displays. +- Look for scale you want and select it, click Apply. + +  +  +  + + +### Bonus Step (for former Mac users) Reduce Font Scaling to Match Your Needs + +We received feedback that for users coming from OS X, installing GNOME Tweaks, browsing to Fonts, and reducing the font size from 1.00 to 0.80 may be preferred. Your own display may vary, so note any changes made if you need to revert back. + +- Goto Displays, set scaling to 200%. This will look too large, so let's fix the fonts. + +- Install with: + +``` +sudo dnf install gnome-tweaks -y +``` + +- Open Tweaks by using the "Super" or Windows key, search tweaks, and enter. + +- At the top, select fonts. Now in that panel, scroll all the way down. Look for Size. Change from 1.00 to 0.80. Close Tweaks. This will vary depending on what you are using for fractional scaling under Displays. + + + +  +  +  diff --git a/framework12/Arch-CachyOS-Setup.md b/framework12/Arch-CachyOS-Setup.md new file mode 100644 index 0000000..5c7a05a --- /dev/null +++ b/framework12/Arch-CachyOS-Setup.md @@ -0,0 +1,315 @@ +# This is for Framework Laptop 12 (Intel Core Series 3) ONLY +## Arch and CachyOS + +### This assumes KDE Plasma is your selected desktop +### This assumes an Arch-based distro running KDE Plasma (e.g. CachyOS or Arch) + +## This will: + +- Get your laptop fully updated. +- Enable tablet rotation mode. +- Install and enable the on-screen (touch) keyboard. + +--- + +### Step 1 — Update your software packages + +- Open a terminal and run: + +``` +sudo pacman -Syu +``` + +- Let it finish completely. **Never interrupt an update partway through.** +- **If the update installed a new kernel, reboot now** before moving on. + +--- + +### Step 2 — Enable tablet rotation mode + +Tablet rotation needs two things: the **sensor daemon** (`iio-sensor-proxy`) that reads the accelerometer, and a **module load-order fix** so the tablet-mode switch is detected. + +#### 2a. Install the sensor daemon + +**This is where Arch and CachyOS differ. Follow the section for your distro.** + +**If you are on Arch:** + +- `iio-sensor-proxy` is **not** installed by default. Install it: + +``` +sudo pacman -S iio-sensor-proxy +``` + +**If you are on CachyOS:** + +- It may already be present. Check first: + +``` +pacman -Qs iio-sensor-proxy +``` + +- If that returns a result, it's installed — move on to 2b. +- If it returns nothing, install it: + +``` +sudo pacman -S iio-sensor-proxy +``` + +> Without this package, KDE's auto-rotate option stays greyed out — the module fix below is not enough on its own. + +#### 2b. Create the modprobe file + +*(Same for both Arch and CachyOS.)* + +``` +sudo nano /etc/modprobe.d/99-fw-tabletmode.conf +``` + +Paste in: + +``` +softdep soc_button_array pre: pinctrl_intel_platform +``` + +- Save and exit: press **Ctrl+O**, then **Enter**, then **Ctrl+X**. + +#### 2c. Edit the mkinitcpio config + +``` +sudo nano /etc/mkinitcpio.conf +``` + +- Find the line that starts with `MODULES=()`. +- Add `pinctrl_intel_platform` inside the parentheses: + - If the line is **empty**, make it exactly: + +``` +MODULES=(pinctrl_intel_platform) +``` + + - If it **already has modules listed**, add ours to the end, space-separated. Example: + +``` +MODULES=(existing_module pinctrl_intel_platform) +``` + +- Save and exit: press **Ctrl+O**, then **Enter**, then **Ctrl+X**. + +#### 2d. Rebuild the initramfs + +``` +sudo mkinitcpio -P +``` + +#### 2e. Reboot + +- **Tablet rotation will not work until you reboot.** + +#### 2f. Verify the sensor is detected + +- After rebooting, run: + +``` +monitor-sensor --accel +``` + +- If it's working, you'll see a line confirming an accelerometer was found, and the readings change as you tilt the laptop. Press **Ctrl+C** to stop. +- Screen rotation should now work when you flip into tablet mode. + +--- + +### Step 3 — Install and enable the on-screen keyboard + +The touch keyboard for KDE Plasma is **Plasma Keyboard**. + +**1. Check whether it's already installed:** + +``` +pacman -Qs plasma-keyboard +``` + +- If it lists a result, it's already installed — skip to enabling it below. + +**2. If it's not installed, install it:** + +``` +sudo pacman -S plasma-keyboard +``` + +**3. Enable it in KDE:** + +- Open **System Settings**. +- Go to **Keyboard → Virtual Keyboard**. +- Select **Plasma Keyboard**. +- Click **Apply**. + +**4. Log out and back in** (or reboot) so KDE picks up the keyboard. + +--- + +### Good to know + +- **Future updates won't undo this.** Your edits live in `/etc/mkinitcpio.conf` and `/etc/modprobe.d/`, which updates don't overwrite. New kernels automatically rebuild the initramfs using your settings — you do **not** need to redo these steps. +- You only need to re-run `sudo mkinitcpio -P` if **you** change one of these config files yourself. +- `iio-sensor-proxy` and the keyboard selection also persist across updates — no need to reinstall or re-enable them. + +--- +--- +--- + +# This is for Framework Laptop 12 (13th Gen Intel Core) ONLY +## Arch and CachyOS + +### This assumes KDE Plasma is your selected desktop +### This assumes an Arch-based distro running KDE Plasma (e.g. CachyOS or Arch) + +## This will: + +- Get your laptop fully updated. +- Enable tablet rotation mode. +- Install and enable the on-screen (touch) keyboard. + +--- + +### Step 1 — Update your software packages + +- Open a terminal and run: + +``` +sudo pacman -Syu +``` + +- Let it finish completely. **Never interrupt an update partway through.** +- **If the update installed a new kernel, reboot now** before moving on. + +--- + +### Step 2 — Enable tablet rotation mode + +Tablet rotation needs two things: the **sensor daemon** (`iio-sensor-proxy`) that reads the accelerometer, and a **module load-order fix** so the tablet-mode switch is detected. + +#### 2a. Install the sensor daemon + +**This is where Arch and CachyOS differ. Follow the section for your distro.** + +**If you are on Arch:** + +- `iio-sensor-proxy` is **not** installed by default. Install it: + +``` +sudo pacman -S iio-sensor-proxy +``` + +**If you are on CachyOS:** + +- It may already be present. Check first: + +``` +pacman -Qs iio-sensor-proxy +``` + +- If that returns a result, it's installed — move on to 2b. +- If it returns nothing, install it: + +``` +sudo pacman -S iio-sensor-proxy +``` + +> Without this package, KDE's auto-rotate option stays greyed out — the module fix below is not enough on its own. + +#### 2b. Create the modprobe file + +*(Same for both Arch and CachyOS.)* + +``` +sudo nano /etc/modprobe.d/99-fw-tabletmode.conf +``` + +Paste in: + +``` +softdep soc_button_array pre: pinctrl_tigerlake +``` + +- Save and exit: press **Ctrl+O**, then **Enter**, then **Ctrl+X**. + +#### 2c. Edit the mkinitcpio config + +``` +sudo nano /etc/mkinitcpio.conf +``` + +- Find the line that starts with `MODULES=()`. +- Add `pinctrl_tigerlake` inside the parentheses: + - If the line is **empty**, make it exactly: + +``` +MODULES=(pinctrl_tigerlake) +``` + + - If it **already has modules listed**, add ours to the end, space-separated. Example: + +``` +MODULES=(existing_module pinctrl_tigerlake) +``` + +- Save and exit: press **Ctrl+O**, then **Enter**, then **Ctrl+X**. + +#### 2d. Rebuild the initramfs + +``` +sudo mkinitcpio -P +``` + +#### 2e. Reboot + +- **Tablet rotation will not work until you reboot.** + +#### 2f. Verify the sensor is detected + +- After rebooting, run: + +``` +monitor-sensor --accel +``` + +- If it's working, you'll see a line confirming an accelerometer was found, and the readings change as you tilt the laptop. Press **Ctrl+C** to stop. +- Screen rotation should now work when you flip into tablet mode. + +--- + +### Step 3 — Install and enable the on-screen keyboard + +The touch keyboard for KDE Plasma is **Plasma Keyboard**. + +**1. Check whether it's already installed:** + +``` +pacman -Qs plasma-keyboard +``` + +- If it lists a result, it's already installed — skip to enabling it below. + +**2. If it's not installed, install it:** + +``` +sudo pacman -S plasma-keyboard +``` + +**3. Enable it in KDE:** + +- Open **System Settings**. +- Go to **Keyboard → Virtual Keyboard**. +- Select **Plasma Keyboard**. +- Click **Apply**. + +**4. Log out and back in** (or reboot) so KDE picks up the keyboard. + +--- + +### Good to know + +- **Future updates won't undo this.** Your edits live in `/etc/mkinitcpio.conf` and `/etc/modprobe.d/`, which updates don't overwrite. New kernels automatically rebuild the initramfs using your settings — you do **not** need to redo these steps. +- You only need to re-run `sudo mkinitcpio -P` if **you** change one of these config files yourself. +- `iio-sensor-proxy` and the keyboard selection also persist across updates — no need to reinstall or re-enable them. diff --git a/framework12/Arch-accel.md b/framework12/Arch-accel.md new file mode 100644 index 0000000..10dc97e --- /dev/null +++ b/framework12/Arch-accel.md @@ -0,0 +1,57 @@ + +**Update as of Sept 14th 2026**, there is a regression it looks like, monitor-sensor --accel will correctly show rotation, but the module ordering is not working correctly. +Please [follow this working guide here](https://github.com/FrameworkComputer/linux-docs/blob/main/framework12/Arch-CachyOS-Setup.md#this-is-for-framework-laptop-12-13th-gen-intel-core-only). + + + +--- +--- + +# Arch Linux Tablet Mode Setup + +This guide will help you enable automatic screen rotation on Arch Linux and its derivatives. On many systems, the required package is not installed by default, and the available version may require a workaround to function correctly. + +> Rather not deal with this at all? [Bazzite](https://guides.frame.work/Guide/Bazzite+Installation+on+the+Framework+Laptop+12/409?lang=en) and [Fedora](https://guides.frame.work/Guide/Fedora+42+Installation+on+the+Framework+Laptop+12/410?lang=en) have this working out of the box, with zero configuration required. + +On a standard Arch Linux installation, the `iio-sensor-proxy` package that manages accelerometer data is not installed. Furthermore, some repositories may provide version 3.7, which has [a bug](https://gitlab.freedesktop.org/hadess/iio-sensor-proxy/-/issues/411) preventing it from delivering sensor events to your desktop environment (GNOME, KDE, etc.). + +The following steps will guide you through installing the package and applying the necessary fix. + +### Step 1: Install `iio-sensor-proxy` + +First, open a terminal and install the package using `pacman`. + +```bash +sudo pacman -S iio-sensor-proxy +```` + +### Step 2: Apply the udev Workaround + +Next, apply the one-line command to fix the bug. This command comments out the problematic rule and reloads the system services. + +```bash +sed 's/.*iio-buffer-accel/#&/' /usr/lib/udev/rules.d/80-iio-sensor-proxy.rules | sudo tee /etc/udev/rules.d/80-iio-sensor-proxy.rules +sudo udevadm trigger --settle +sudo systemctl restart iio-sensor-proxy +``` + +### Step 3: Verify the Fix + +Finally, you can check if screen rotation is working correctly. + +```bash +monitor-sensor --accel +``` + +You should see the following output, confirming the accelerometer is detected: + +``` + Waiting for iio-sensor-proxy to appear ++++ iio-sensor-proxy appeared +=== Has accelerometer (orientation: normal) +``` + +> Tablet rotation mode should now work immediately. However, if for some reason it does not, reboot your computer and then test rotation again. Remember to flip the screen completely back to test rotation properly. + +``` +``` diff --git a/framework12/Fedora-all.md b/framework12/Fedora-all.md new file mode 100644 index 0000000..a6ba397 --- /dev/null +++ b/framework12/Fedora-all.md @@ -0,0 +1,250 @@ +# This is for Framework Laptop 12 (13th Gen Intel Core) ONLY + +### Fedora Workstation (GNOME ONLY) ([Tablet mode GNOME and KDE Plasma Desktop](https://github.com/FrameworkComputer/linux-docs/blob/main/framework12/Fedora-all.md#step-2-workstationgnome-tablet-mode-gnome-and-kde-plasma-desktop---get-tablet-rotation-mode-working)) + +## This will: + +- Getting your laptop fully updated. +- Fix tablet rotation mode +- Enable improved fractional scaling support Fedora's GNOME environment using Wayland. +- Enabling tap to click on the touchpad. + +  +  +  + +### Step 1 (Workstation/GNOME) Updating your software packages + +- Browse to the horizontal line in the upper left corner, click to open it. +- Type out the word terminal, click to open it. +- Copy the code below in the gray box, right click/paste it into the terminal window. +- Then press the enter key, user password, enter key, **reboot.** + + +``` +sudo dnf upgrade +``` +> **TIP:** You can use the little clipboard icon to the right of the code to copy to your clipboard. + + +**Reboot** + +  +  +  + +### Step 2 (Workstation/GNOME) ([Tablet mode GNOME and KDE Plasma Desktop](https://github.com/FrameworkComputer/linux-docs/blob/main/framework12/Fedora-all.md#step-2-workstationgnome-tablet-mode-gnome-and-kde-plasma-desktop---get-tablet-rotation-mode-working)) - Get tablet rotation mode working: + +- This will be fixed upstream, but this is a workaround to get things in the correct working order. + +- Create the following file. + + ``` + sudo nano /etc/dracut.conf.d/99-fw-tabletmode.conf + ``` + + - Paste in: + + ``` + omit_drivers+=" soc_button_array " + force_drivers+=" pinctrl_tigerlake " + ``` + +- Ctrl x, then save. + +- Next, create the following file. + + ``` + sudo nano /etc/modprobe.d/99-fw-tabletmode.conf + ``` + + - Paste in: + + ``` + softdep soc_button_array pre: pinctrl_tigerlake + ``` +- Ctrl x, then save. + +Run: + +``` +sudo dracut -f +``` + +Then reboot. + + +  +  +  + +### Step 3 (Workstation/GNOME) - If you want to enable fractional scaling on Wayland: + +- Type out the word Displays. +- Look for scale you want and select it, click Apply. + +  +  +  + +### Step 4 (Workstation/GNOME) - If you want to enable "tap-to-click" on the touchpad: + +- Browse to the horizontal line in the upper left corner, click to open it. +- Type out the word mouse, look for Mouse and Touchpad, click to open it. +- Click the touchpad option at the top. +- Under "Clicking", select Tap to Click and enable it. + +  +  +  +### Bonus Step (Workstation/GNOME) (for former Mac users) Reduce Font Scaling to Match Your Needs + +We received feedback that for users coming from OS X, installing GNOME Tweaks, browsing to Fonts, and reducing the font size from 1.00 to 0.80 may be preferred. + +- Goto Displays, set scaling to 200%. This will look too large, so let's fix the fonts. +- Install with: + +``` +sudo dnf install gnome-tweaks -y +``` + +- Open Tweaks by using the "Super" or Windows key, search tweaks, and enter. + +- At the top, select fonts. Now in that panel, scroll all the way down. Look for Size. Change from 1.00 to 0.80. Close Tweaks. This will vary depending on what you are using for fractional scaling under Displays. + + Note: This is for the displays for the laptop only. This will look super odd on external displays and likely too large even still. + + +  +  +  + + + + +-------------------------------- +-------------------------------- + + + +# This is for Framework Laptop 12 (Core Series 3) ONLY +### Fedora Workstation (GNOME) For Core Series 3 on KDE Plasma, [we have a specific OEM flow we recommend instead](https://guides.frame.work/Guide/Fedora+KDE+Plasma+Desktop+Installation+on+the+Framework+Laptop+12+Intel%C2%AE+Core%E2%84%A2+Series+3/856#s4404). + +## This will: + +- Getting your laptop fully updated. +- Fix tablet rotation mode +- Enable improved fractional scaling support Fedora's GNOME environment using Wayland. +- Enabling tap to click on the touchpad. + +  +  +  + +### Step 1 (Workstation/GNOME) Updating your software packages + +- Browse to the horizontal line in the upper left corner, click to open it. +- Type out the word terminal, click to open it. +- Copy the code below in the gray box, right click/paste it into the terminal window. +- Then press the enter key, user password, enter key, **reboot.** + + +``` +sudo dnf upgrade +``` +> **TIP:** You can use the little clipboard icon to the right of the code to copy to your clipboard. + + +**Reboot** + +  +  +  + +### Step 2 (Workstation/GNOME) ([Tablet mode GNOME](https://github.com/FrameworkComputer/linux-docs/blob/main/framework12/Fedora-all.md#step-2-workstationgnome-tablet-mode-gnome---get-tablet-rotation-mode-working)) - Get tablet rotation mode working: + +- This will be fixed upstream, but this is a workaround to get things in the correct working order. + +- Create the following file. + + ``` + sudo nano /etc/dracut.conf.d/99-fw-tabletmode.conf + ``` + + - Paste in: + + ``` + omit_drivers+=" soc_button_array " + force_drivers+=" pinctrl_intel_platform " + ``` + +- Ctrl x, t hen save. + +- Next, create the following file. + + ``` + sudo nano /etc/modprobe.d/99-fw-tabletmode.conf + ``` + + - Paste in: + + ``` + softdep soc_button_array pre: pinctrl_intel_platform + ``` +- Ctrl x, t hen save. + +Run: + +``` +sudo dracut -f +``` + +Then reboot. + + +  +  +  + +### Step 3 (Workstation/GNOME) - If you want to enable fractional scaling on Wayland: + +- Type out the word Displays. +- Look for scale you want and select it, click Apply. + +  +  +  + +### Step 4 (Workstation/GNOME) - If you want to enable "tap-to-click" on the touchpad: + +- Browse to the horizontal line in the upper left corner, click to open it. +- Type out the word mouse, look for Mouse and Touchpad, click to open it. +- Click the touchpad option at the top. +- Under "Clicking", select Tap to Click and enable it. + +  +  +  +### Bonus Step (Workstation/GNOME) (for former Mac users) Reduce Font Scaling to Match Your Needs + +We received feedback that for users coming from OS X, installing GNOME Tweaks, browsing to Fonts, and reducing the font size from 1.00 to 0.80 may be preferred. + +- Goto Displays, set scaling to 200%. This will look too large, so let's fix the fonts. +- Install with: + +``` +sudo dnf install gnome-tweaks -y +``` + +- Open Tweaks by using the "Super" or Windows key, search tweaks, and enter. + +- At the top, select fonts. Now in that panel, scroll all the way down. Look for Size. Change from 1.00 to 0.80. Close Tweaks. This will vary depending on what you are using for fractional scaling under Displays. + + Note: This is for the displays for the laptop only. This will look super odd on external displays and likely too large even still. + + + +  +  +  + diff --git a/framework12/Fedora42.md b/framework12/Fedora42.md new file mode 100644 index 0000000..5181d6b --- /dev/null +++ b/framework12/Fedora42.md @@ -0,0 +1,75 @@ +# This is for Framework Laptop 12 ONLY +### Fedora Workstation (GNOME) + +## This will: + +- Getting your laptop fully updated. +- Enable improved fractional scaling support Fedora's GNOME environment using Wayland. +- Enabling tap to click on the touchpad. + +  +  +  + + +### Step 1 Updating your software packages + +- Browse to the horizontal line in the upper left corner, click to open it. +- Type out the word terminal, click to open it. +- Copy the code below in the gray box, right click/paste it into the terminal window. +- Then press the enter key, user password, enter key, **reboot.** + + +``` +sudo dnf upgrade +``` +> **TIP:** You can use the little clipboard icon to the right of the code to copy to your clipboard. + + +**Reboot** + +  +  +  + +### Step 2 - If you want to enable fractional scaling on Wayland: + +- Type out the word Displays. +- Look for scale you want and select it, click Apply. + +  +  +  + +### Step 3 - If you want to enable "tap-to-click" on the touchpad: + +- Browse to the horizontal line in the upper left corner, click to open it. +- Type out the word mouse, look for Mouse and Touchpad, click to open it. +- Click the touchpad option at the top. +- Under "Clicking", select Tap to Click and enable it. + +  +  +  +### Bonus Step (for former Mac users) Reduce Font Scaling to Match Your Needs + +We received feedback that for users coming from OS X, installing GNOME Tweaks, browsing to Fonts, and reducing the font size from 1.00 to 0.80 may be preferred. + +- Goto Displays, set scaling to 200%. This will look too large, so let's fix the fonts. +- Install with: + +``` +sudo dnf install gnome-tweaks -y +``` + +- Open Tweaks by using the "Super" or Windows key, search tweaks, and enter. + +- At the top, select fonts. Now in that panel, scroll all the way down. Look for Size. Change from 1.00 to 0.80. Close Tweaks. This will vary depending on what you are using for fractional scaling under Displays. + + Note: This is for the displays for the laptop only. This will look super odd on external displays and likely too large even still. + + + +  +  +  diff --git a/framework12/Ubuntu-24-04.md b/framework12/Ubuntu-24-04.md new file mode 100644 index 0000000..21847d0 --- /dev/null +++ b/framework12/Ubuntu-24-04.md @@ -0,0 +1,55 @@ +# Ubuntu 24.04 on Framework Laptop 12 (13th Gen Intel® Core™) ONLY. + + +## This will: + +- Update your Ubuntu install's packages. + +        + + +### Get everything updated + +- Browse to the upper left corner, click the horizontal line to open the menu. +- Type out the word terminal, click to open it. +- Click on the small icon shown in the image below to copy the code below in the gray box, right click/paste it into the terminal window. +- Then press the enter key, user password, enter key, **reboot.** + +``` +sudo apt update && sudo apt upgrade -y && sudo snap refresh +``` +> **TIP:** You can use the little clipboard icon to the right of the code to copy to your clipboard. + +

    Copy The Code Below Like This

    + + +      + + +### Bonus Step (for former Mac users) Reduce Font Scaling to Match Your Needs + +We received feedback that for users coming from OS X, installing GNOME Tweaks, browsing to Fonts, and reducing the font size from 1.00 to 0.80 may be preferred. This will vary greatly how you have your fractional scaling setup in the Displays settings area. + +- Goto Displays, set scaling to 200%. This will look too large, so let's fix the fonts. +- Install with: + +``` +sudo apt update && sudo apt install gnome-tweaks -y +``` + +- Open Tweaks by using the "Super" or Windows key, search tweaks, and enter. + +- At the top, select fonts. Now in that panel, scroll all the way down. Look for Size. Change from 1.00 to 0.80. Close Tweaks. + + Note: This is for the displays for the laptop only. This will look super odd on external displays and likely too large even still. + +### Bonus Step - Correct blurry text rendering in the Chrome browser + + - Open your Chrome browser, browse to chrome://flags/ and press enter. + - Look for the search box at the top of the page, type in the words _ozone platform_ then press the enter key. + - Look for the box marked Default, change it to Auto. + - With this changed to Auto, relaunch your Chrome browser. + +![ozone platform](https://raw.githubusercontent.com/FrameworkComputer/linux-docs/refs/heads/main/framework12/images/oszone.png) + + diff --git a/framework12/Ubuntu-25-04-accel-ubuntu25.04.md b/framework12/Ubuntu-25-04-accel-ubuntu25.04.md new file mode 100644 index 0000000..db53833 --- /dev/null +++ b/framework12/Ubuntu-25-04-accel-ubuntu25.04.md @@ -0,0 +1,143 @@ +# Ubuntu 25.04+ Tablet Mode Setup Udev Edit +## 25.04 and 25.10 both apply to this guide + +This guide will help set up screen rotation support for your laptop on Ubuntu 25.04/25.10, giving you an experience similar to what Fedora 42 and Bazzite offer out of the box. + +> Rather not deal with this at all? [Bazzite](https://guides.frame.work/Guide/Bazzite+Installation+on+the+Framework+Laptop+12/409?lang=en) and [Fedora](https://guides.frame.work/Guide/Fedora+42+Installation+on+the+Framework+Laptop+12/410?lang=en) are ready to go out of the box, zero configuration. + +Ubuntu 25.04 currently ships with iio-sensor-proxy 3.7 that has [a bug](https://gitlab.freedesktop.org/hadess/iio-sensor-proxy/-/issues/411) preventing it from delivering accelerometer events from kernel to userspace (GNOME, KDE, ...). + +We have [submitted a bug](https://bugs.launchpad.net/ubuntu/+source/iio-sensor-proxy/+bug/2117530) to backport the upstream fix or update to 3.8. +In the meanwhile you can use the following workaround: + +``` +sed 's/.*iio-buffer-accel/#&/' /usr/lib/udev/rules.d/80-iio-sensor-proxy.rules | sudo tee /etc/udev/rules.d/80-iio-sensor-proxy.rules +sudo udevadm trigger --settle +sudo systemctl restart iio-sensor-proxy +``` + +Then you can check if screen rotation works: + +``` +> monitor-sensor --accel + Waiting for iio-sensor-proxy to appear ++++ iio-sensor-proxy appeared +=== Has accelerometer (orientation: normal) +``` + +> Tablet rotation mode should work immediately. Howver if for some reason it does not, reboot then test rotation again. Remember to flip the the screen completely back to test rotation properly. + + +--------------------------------------------------- + +### Temporary workaround for a 25.10 tablet mode bug + +I will be getting this in front of engineering, _but in the meantime_. this is a workaround based [on this thread](https://community.frame.work/t/ubuntu-25-10-on-framework-laptop-12/77416/16?u=matt_hartley). + +This has been heavily tested and provides an immediate workaround for now. + +Terminal, paste, enter, password when prompted, done. **No ./script stuff.** + +**Enable workaround** + +``` +sudo bash -c ' + +echo "Creating /usr/local/sbin/reload-soc-module.sh..." +{ + mkdir -p /usr/local/sbin && + cat << "EOF" > /usr/local/sbin/reload-soc-module.sh +#!/bin/bash +echo "Removing soc_button_array..." +if ! rmmod soc_button_array 2>/dev/null; then + echo "Warning: soc_button_array was not loaded or could not be removed." +fi + +echo "Loading soc_button_array..." +if ! modprobe soc_button_array; then + echo "ERROR: Failed to load soc_button_array module." + exit 1 +fi + +echo "soc_button_array reloaded successfully." +EOF +} || { echo "ERROR: Failed to create reload-soc-module.sh"; exit 1; } + +chmod +x /usr/local/sbin/reload-soc-module.sh + +echo "Creating systemd service..." +{ + cat << "EOF" > /etc/systemd/system/reload-soc-module.service +[Unit] +Description=Ubuntu 25.10 workaround to reload soc_button_array +After=network.target + +[Service] +Type=simple +ExecStart=/usr/local/sbin/reload-soc-module.sh + +[Install] +WantedBy=multi-user.target +EOF +} || { echo "ERROR: Failed to create systemd service file"; exit 1; } + +echo "Reloading systemd..." +if ! systemctl daemon-reload; then + echo "ERROR: systemctl daemon-reload failed." + exit 1 +fi + +echo "Enabling service..." +if ! systemctl enable reload-soc-module.service; then + echo "ERROR: Failed to enable reload-soc-module.service" + exit 1 +fi + +echo "Starting service..." +if ! systemctl start reload-soc-module.service; then + echo "ERROR: Failed to start reload-soc-module.service" + exit 1 +fi + +echo "SUCCESS: soc_button_array reload service installed and running." + +' +``` + +**Later on when a fix has been provided, this is the undo method** + +``` +sudo bash -c ' + +echo "Stopping reload-soc-module.service..." +if ! systemctl stop reload-soc-module.service 2>/dev/null; then + echo "Warning: Service was not running or could not be stopped." +fi + +echo "Disabling reload-soc-module.service..." +if ! systemctl disable reload-soc-module.service 2>/dev/null; then + echo "Warning: Service could not be disabled (may not exist)." +fi + +echo "Removing service file..." +if ! rm -f /etc/systemd/system/reload-soc-module.service; then + echo "ERROR: Could not remove /etc/systemd/system/reload-soc-module.service" + exit 1 +fi + +echo "Removing /usr/local/sbin/reload-soc-module.sh..." +if ! rm -f /usr/local/sbin/reload-soc-module.sh; then + echo "ERROR: Could not remove /usr/local/sbin/reload-soc-module.sh" + exit 1 +fi + +echo "Reloading systemd..." +if ! systemctl daemon-reload; then + echo "ERROR: systemctl daemon-reload failed." + exit 1 +fi + +echo "Undo complete: soc reload workaround fully removed - REBOOT." + +' +``` diff --git a/framework12/Ubuntu-25-04.md b/framework12/Ubuntu-25-04.md new file mode 100644 index 0000000..4866a15 --- /dev/null +++ b/framework12/Ubuntu-25-04.md @@ -0,0 +1,58 @@ +# This is for Ubuntu 25.04 on Framework Laptop 12 ONLY. + + +## This will: + +- Update your Ubuntu install's packages. +- Walk you getting tablet mode setup for Ubuntu 25.04 (ONLY) + +        + + +### Get everything updated + +- Browse to the upper left corner, click the horizontal line to open the menu. +- Type out the word terminal, click to open it. +- Click on the small icon shown in the image below to copy the code below in the gray box, right click/paste it into the terminal window. +- Then press the enter key, user password, enter key, **reboot.** + +``` +sudo apt update && sudo apt upgrade -y && sudo snap refresh +``` +> **TIP:** You can use the little clipboard icon to the right of the code to copy to your clipboard. + +

    Copy The Code Below Like This

    + +- **Then reboot** + +      + +## Tablet mode on Ubuntu + + +- Only works on 25.04 +- Needs below fixup until they have updated iio-sensor-proxy to 3.8: [![Ubuntu 25.04 package](https://repology.org/badge/version-for-repo/ubuntu_25_04/iio-sensor-proxy.svg)](https://repology.org/project/iio-sensor-proxy/versions) +- [This script will get tablet mode set up and running fast](https://github.com/FrameworkComputer/linux-docs/blob/main/framework12/Ubuntu-25-04-accel-ubuntu25.04.md#ubuntu-2504-tablet-mode-setup-udev-edit). +- Onscreen keyboard only appears when you call for it by interacting in a text area. + +![Tablet Mode](https://raw.githubusercontent.com/FrameworkComputer/linux-docs/refs/heads/main/framework12/images/tablet.png) + +      + + +### Bonus Step (for former Mac users) Reduce Font Scaling to Match Your Needs + +We received feedback that for users coming from OS X, installing GNOME Tweaks, browsing to Fonts, and reducing the font size from 1.00 to 0.80 may be preferred. This will vary greatly how you have your fractional scaling setup in the Displays settings area. + +- Goto Displays, set scaling to 200%. This will look too large, so let's fix the fonts. +- Install with: + +``` +sudo apt update && sudo apt install gnome-tweaks -y +``` + +- Open Tweaks by using the "Super" or Windows key, search tweaks, and enter. + +- At the top, select fonts. Now in that panel, scroll all the way down. Look for Size. Change from 1.00 to 0.80. Close Tweaks. + + Note: This is for the displays for the laptop only. This will look super odd on external displays and likely too large even still. diff --git a/framework12/Ubuntu-25-10.md b/framework12/Ubuntu-25-10.md new file mode 100644 index 0000000..a71ef4f --- /dev/null +++ b/framework12/Ubuntu-25-10.md @@ -0,0 +1,108 @@ +# This is for Ubuntu 25.10 on Framework Laptop 12 ONLY. + + +## This will: + +- Update your Ubuntu install's packages. +- Walk you getting tablet mode setup for Ubuntu 25.10 (ONLY) + +        + + +### Get everything updated + +- Browse to the upper left corner, click the horizontal line to open the menu. +- Type out the word terminal, click to open it. +- Click on the small icon shown in the image below to copy the code below in the gray box, right click/paste it into the terminal window. +- Then press the enter key, user password, enter key, **reboot.** + +``` +sudo apt update && sudo apt upgrade -y && sudo snap refresh +``` +> **TIP:** You can use the little clipboard icon to the right of the code to copy to your clipboard. + +

    Copy The Code Below Like This

    + +- **Then reboot** + +      + +## Tablet mode on Ubuntu + + +- Only works on 25.04+ (and up) +- Needs below fixup until they have updated iio-sensor-proxy to 3.8: [![Ubuntu 25.10 package](https://repology.org/badge/version-for-repo/ubuntu_25_10/iio-sensor-proxy.svg)](https://repology.org/project/iio-sensor-proxy/versions) +- [This script will get tablet mode set up and running fast](https://github.com/FrameworkComputer/linux-docs/blob/main/framework12/Ubuntu-25-04-accel-ubuntu25.04.md#ubuntu-2504-tablet-mode-setup-udev-edit). +- Onscreen keyboard only appears when you call for it by interacting in a text area. + +### **Bug** + +- There is a bug where Ubuntu is not providing kernel recognized the tabletmode GPIO. Please use this workaround: +``` +sudo nano /etc/initramfs-tools/modules +``` +Addpenf this to the bottom of the file: +``` +# Ensure pinctrl_tigerlake loads before soc_button_array +pinctrl_tigerlake +soc_button_array +``` +Update initramfs: +``` +sudo update-initramfs -u -k all +``` +Then reboot. Tablet mode will work now. + + +![Tablet Mode](https://raw.githubusercontent.com/FrameworkComputer/linux-docs/refs/heads/main/framework12/images/tablet2.png) + +      + + +### Bonus Step (for former Mac users) Reduce Font Scaling to Match Your Needs + +We received feedback that for users coming from OS X, installing GNOME Tweaks, browsing to Fonts, and reducing the font size from 1.00 to 0.80 may be preferred. This will vary greatly how you have your fractional scaling setup in the Displays settings area. + +- Goto Displays, set scaling to 200%. This will look too large, so let's fix the fonts. +- Install with: + +``` +sudo apt update && sudo apt install gnome-tweaks -y +``` + +- Open Tweaks by using the "Super" or Windows key, search tweaks, and enter. + +- At the top, select fonts. Now in that panel, scroll all the way down. Look for Size. Change from 1.00 to 0.80. Close Tweaks. + + Note: This is for the displays for the laptop only. This will look super odd on external displays and likely too large even still. + +### Install issues + +If you run into an installation issue where the previous release; 25.04 installed fine. We would recommend installing the previous release and then upgrading in place. + + +**Update** +``` +sudo apt update && sudo apt upgrade +``` + +**Change to allow non-LTS upgrades** +``` +sudo nano /etc/update-manager/release-upgrades +``` +Change: Prompt=lts +To: Prompt=normal + +**Upgrade to previous release first (required step)** +``` +sudo do-release-upgrade +``` + +**After reboot, upgrade to 25.10 (current release)** +``` +sudo do-release-upgrade +``` + + + + diff --git a/framework12/Ubuntu-26-04-Tablet-Mode.md b/framework12/Ubuntu-26-04-Tablet-Mode.md new file mode 100644 index 0000000..c214fd2 --- /dev/null +++ b/framework12/Ubuntu-26-04-Tablet-Mode.md @@ -0,0 +1,126 @@ +# This is for Framework Laptop 12 (13th Gen Intel Core) ONLY + +### Ubuntu 26.04 (GNOME) + +## This will: + +- Getting your laptop fully updated. +- Fix tablet rotation mode +- Enable improved fractional scaling support on Ubuntu's GNOME environment using Wayland. +- Enabling tap to click on the touchpad. + +  +  +  + +### Step 1 Updating your software packages + +- Browse to the horizontal line in the upper left corner, click to open it. +- Type out the word terminal, click to open it. +- Copy the code below in the gray box, right click/paste it into the terminal window. +- Then press the enter key, user password, enter key, **reboot.** + + +``` +sudo apt update && sudo apt full-upgrade +``` +> **TIP:** You can use the little clipboard icon to the right of the code to copy to your clipboard. + + +**Reboot** + +  +  +  + +### Step 2 - Get tablet rotation mode working: + +- This will be fixed upstream, but this is a workaround to get things in the correct working order. + +- Create the following file. + + ``` + sudo nano /etc/dracut.conf.d/99-fw-tabletmode.conf + ``` + + - Paste in: + + ``` + omit_drivers+=" soc_button_array " + force_drivers+=" pinctrl_tigerlake " + ``` + +- Ctrl x, then save. + +- Next, create the following file. + + ``` + sudo nano /etc/modprobe.d/99-fw-tabletmode.conf + ``` + + - Paste in: + + ``` + softdep soc_button_array pre: pinctrl_tigerlake + ``` +- Ctrl x, then save. + +Run: + +``` +sudo dracut -f +``` + +Then reboot. + + +  +  +  + +### Step 3 - If you want to enable fractional scaling on Wayland: + +- Type out the word Displays. +- Look for scale you want and select it, click Apply. + +  +  +  + +### Step 4 - If you want to enable "tap-to-click" on the touchpad: + +- Browse to the horizontal line in the upper left corner, click to open it. +- Type out the word mouse, look for Mouse and Touchpad, click to open it. +- Click the touchpad option at the top. +- Under "Clicking", select Tap to Click and enable it. + +  +  +  +### Bonus Step (for former Mac users) Reduce Font Scaling to Match Your Needs + +We received feedback that for users coming from OS X, installing GNOME Tweaks, browsing to Fonts, and reducing the font size from 1.00 to 0.80 may be preferred. + +- Goto Displays, set scaling to 200%. This will look too large, so let's fix the fonts. +- Install with: + +``` +sudo apt install gnome-tweaks -y +``` + +- Open Tweaks by using the "Super" or Windows key, search tweaks, and enter. + +- At the top, select fonts. Now in that panel, scroll all the way down. Look for Size. Change from 1.00 to 0.80. Close Tweaks. This will vary depending on what you are using for fractional scaling under Displays. + + Note: This is for the displays for the laptop only. This will look super odd on external displays and likely too large even still. + + + +  +  +  + + + +------ +------ diff --git a/framework12/Ubuntu-26.04.md b/framework12/Ubuntu-26.04.md new file mode 100644 index 0000000..3c92577 --- /dev/null +++ b/framework12/Ubuntu-26.04.md @@ -0,0 +1,31 @@ +# This is for Ubuntu 26.04 on Framework Laptop 12 ONLY. + +## This will: + +- Update your Ubuntu install's packages. +- Walk you getting tablet mode setup for Ubuntu 26.04 (ONLY) + +        + + +### Get everything updated + +- Browse to the upper left corner, click the horizontal line to open the menu. +- Type out the word terminal, click to open it. +- Click on the small icon shown in the image below to copy the code below in the gray box, right click/paste it into the terminal window. +- Then press the enter key, user password, enter key, **reboot.** + +``` +sudo apt update && sudo apt upgrade -y && sudo snap refresh +``` +> **TIP:** You can use the little clipboard icon to the right of the code to copy to your clipboard. + +

    Copy The Code Below Like This

    + +- **Then reboot** + +      + +## Tablet Mode for 26.04 + +Please [follow this guide](https://github.com/FrameworkComputer/linux-docs/blob/main/framework12/Ubuntu-26-04-Tablet-Mode.md#for-ubuntu-2604-tablet-mode) for enabling tablet rotation functionality for Ubuntu 26.04. diff --git a/framework12/debugging.md b/framework12/debugging.md new file mode 100644 index 0000000..c5af074 --- /dev/null +++ b/framework12/debugging.md @@ -0,0 +1,169 @@ +# Framework 12 - Debugging + +Framework 12 has a couple of features not present on our other systems. +They might not work on some installations, these instructions here help you find the root cause and how to fix it. + +## Touchpad + +No special configuration or recent kernel version needed. +Kernel 6.x with libinput can work. + +## Touchscreen + +No special configuration or recent kernel version needed. +Kernel 6.x with libinput can work. + +[Stylus](stylus.md) + +## Tablet Mode + +Kernel drivers required: + +- `pinctrl_tigerlake` (Must be built into the kernel or load first) +- [`soc_button_array`](https://github.com/torvalds/linux/blob/master/drivers/input/misc/soc_button_array.c) + +### Check that both modules are loaded + +``` +> sudo lsmod | grep -e pinctrl_tigerlake -e soc_button_array +pinctrl_tigerlake 24576 0 +soc_button_array 28672 5 +``` + +If no, you can load them manually: + +``` +> sudo modprobe pinctrl_tigerlake soc_button_array +``` + +### Check that the kernel recognized the tabletmode GPIO + +``` +> journalctl -k | grep gpio-keys +Feb 27 19:14:00 fedora kernel: input: gpio-keys as /devices/platform/INT33D3:00/gpio-keys.1.auto/input/input17 +``` + +If no, then `pinctrl_tigerlake` might have loaded after `soc_button_array`. +Reload them manually in the right order: + +``` +sudo rmmod soc_button_array +sudo modprobe soc_button_array +``` + +To make this permanent you can configure your distribution to load +`pinctrl_tigerlake` in initrd. + +### Check that libinput can see tablet mode changes + +``` + > sudo libinput debug-events | grep SWITCH_TOGGLE +-event13 SWITCH_TOGGLE +0.000s switch tablet-mode state 1 + event13 SWITCH_TOGGLE +1.360s switch tablet-mode state 0 +``` + +We have not seen this fail, if the kernel modules are okay, this should work. +If not, please contact Framework. + +## Screen Rotation + +GNOME, KDE, Windows all rotate the screen only if the system is in tablet mode, +so please check if that's working first. + +If you want to rotate in laptop mode, KDE has a setting and GNOME has a plugin to do that. + +The kernel driver +[`cros_ec_sensors`](https://github.com/torvalds/linux/blob/master/drivers/iio/common/cros_ec_sensors/cros_ec_sensors.c) +reads accelerometer from the EC controller. This is supported on Framework 12 +since Linux 6.12. + +iio-sensor-proxy interprets that accelerometer data as a screen orientation and +forwards it to GNOME/KDE via dbus. + +### Check that the EC driver recognizes the system + +``` +> sudo dmesg | grep cros_ec +[ 9.014454] cros_ec_lpcs cros_ec_lpcs.0: loaded with quirks 00000001 +[ 9.025815] cros_ec_lpcs cros_ec_lpcs.0: Chrome EC device registered +``` + +If no, likely your kernel is older than 6.12. + +### Check that the sensor is working + +Run the below command and check that the lid angle and sensor data responds +correctly when you move the device or bend the lid at the hinge. + +``` + +> sudo watch -n1 framework_tool --sensors +Accelerometers: + Lid Angle: 118 Deg + Lid Sensor: X=+0.00G Y=+0.86G, Z=+0.53G + Base Sensor: X=-0.03G Y=-0.07G, Z=+1.02G +``` + +### Check that the kernel exposes accelerometer data + +``` +> cat /sys/bus/iio/devices/iio:device0/{name,label,in_accel_{x,y,z}_raw} +cros-ec-accel +accel-display +-192 +14400 +6672 +``` + +If that is not working, please contact Framework. + +### Check that the iio-sensor-proxy daemon is runnning + +``` +> systemctl status iio-sensor-proxy.service | grep Active + Active: active (running) since Thu 2025-02-27 19:14:02 CST; 19h ago +``` + +If no, make sure the package is installed and the service is enabled and running. + + +### Check that the daemon can be accessed and recognizes the sensor + +``` +> monitor-sensor --accel + Waiting for iio-sensor-proxy to appear ++++ iio-sensor-proxy appeared +=== Has accelerometer (orientation: normal) + Accelerometer orientation changed: right-up + Accelerometer orientation changed: normal +``` + +If not, you are likely running iio-sensor-proxy 3.7, which has a +[known regression](https://gitlab.freedesktop.org/hadess/iio-sensor-proxy/-/merge_requests/400) +that is fixed in iio-sensor-proxy 3.8. +If your distribution has not updated to 3.8, you can either downgrade to +3.6 or remove a line in the udev config: + +``` +sed 's/.*iio-buffer-accel/#&/' /usr/lib/udev/rules.d/80-iio-sensor-proxy.rules | sudo tee /etc/udev/rules.d/80-iio-sensor-proxy.rules +sudo udevadm trigger --settle +sudo systemctl restart iio-sensor-proxy +``` + +Please see our distribution specific documentation for further details. + +Below is the current version in different distributions - only 3.7 is bad. + +- Fedora + - [![Fedora 43 package](https://repology.org/badge/version-for-repo/fedora_43/iio-sensor-proxy.svg)](https://repology.org/project/iio-sensor-proxy/versions) + - [![Fedora 42 package](https://repology.org/badge/version-for-repo/fedora_42/iio-sensor-proxy.svg)](https://repology.org/project/iio-sensor-proxy/versions) +- NixOS + - [![nixpkgs stable 25.11 package](https://repology.org/badge/version-for-repo/nix_stable_25_11/iio-sensor-proxy.svg)](https://repology.org/project/iio-sensor-proxy/versions) + - [![nixpkgs unstable package](https://repology.org/badge/version-for-repo/nix_unstable/iio-sensor-proxy.svg)](https://repology.org/project/iio-sensor-proxy/versions) +- Ubuntu + - [![Ubuntu 24.04 LTS package](https://repology.org/badge/version-for-repo/ubuntu_24_04/iio-sensor-proxy.svg)](https://repology.org/project/iio-sensor-proxy/versions) + - [![Ubuntu 25.10 package](https://repology.org/badge/version-for-repo/ubuntu_25_10/iio-sensor-proxy.svg)](https://repology.org/project/iio-sensor-proxy/versions) +- [![Arch Linux package](https://repology.org/badge/version-for-repo/arch/iio-sensor-proxy.svg)](https://repology.org/project/iio-sensor-proxy/versions) +- Debian + - [![Debian 12 package](https://repology.org/badge/version-for-repo/debian_12/iio-sensor-proxy.svg)](https://repology.org/project/iio-sensor-proxy/versions) + - [![Debian 13 package](https://repology.org/badge/version-for-repo/debian_13/iio-sensor-proxy.svg)](https://repology.org/project/iio-sensor-proxy/versions) diff --git a/framework12/images/install.png b/framework12/images/install.png new file mode 100644 index 0000000..dabf3e1 Binary files /dev/null and b/framework12/images/install.png differ diff --git a/framework12/images/oszone.png b/framework12/images/oszone.png new file mode 100644 index 0000000..133c36f Binary files /dev/null and b/framework12/images/oszone.png differ diff --git a/framework12/images/readme b/framework12/images/readme new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/framework12/images/readme @@ -0,0 +1 @@ + diff --git a/framework12/images/tablet-1.png b/framework12/images/tablet-1.png new file mode 100644 index 0000000..9a330c3 Binary files /dev/null and b/framework12/images/tablet-1.png differ diff --git a/framework12/images/tablet.png b/framework12/images/tablet.png new file mode 100644 index 0000000..1cf271c Binary files /dev/null and b/framework12/images/tablet.png differ diff --git a/framework12/images/tablet2.png b/framework12/images/tablet2.png new file mode 100644 index 0000000..afcef98 Binary files /dev/null and b/framework12/images/tablet2.png differ diff --git a/framework12/nixOS.md b/framework12/nixOS.md new file mode 100644 index 0000000..090b2a2 --- /dev/null +++ b/framework12/nixOS.md @@ -0,0 +1,120 @@ +# Adding the NixOS-Hardware Module (Framework 12, 13th Gen Intel Core) + +This provides the hardware channel steps for the [NixOS on the Framework Laptop 12 Guide](https://guides.frame.work/Guide/NixOS+on+the+Framework+Laptop+12/412?lang=en) + +## Channel-based (default from graphical installer) + +```bash +sudo nix-channel --add https://github.com/NixOS/nixos-hardware/archive/master.tar.gz nixos-hardware +sudo nix-channel --update +``` + +```nix +# /etc/nixos/configuration.nix +imports = [ + ./hardware-configuration.nix + +]; +``` + +```bash +sudo nixos-rebuild switch +``` + +## Flake-Based + +```nix +# flake.nix inputs +inputs.nixos-hardware.url = "github:NixOS/nixos-hardware/master"; +``` + +```nix +# flake.nix modules list +modules = [ + ./configuration.nix + nixos-hardware.nixosModules.framework-12-13th-gen-intel +]; +``` + +```bash +sudo nixos-rebuild switch --flake .# +``` + + +## Enabling the accelerometer and tablet mode +Tablet mode signals the desktop environment that the keyboard is folded back. libinput disables the keyboard and touchpad - firmware also does that. And GNOME/KDE enable screen rotation based on the accelerometer, see below. + +Enable the following: + +```nix +# Framework 12 tablet mode. + boot.initrd.kernelModules = [ "pinctrl_tigerlake" ]; + hardware.sensor.iio.enable = true; + +# On-screen keyboard (KDE Plasma). + environment.systemPackages = with pkgs; [ kdePackages.plasma-keyboard ]; +``` + +`plasma-keyboard` is the KDE Plasma on-screen keyboard, so this applies to Plasma sessions. With the physical keyboard and touchpad disabled in tablet mode, it gives you a way to type when the display is folded back. GNOME ships its own on-screen keyboard, so this package isn't needed there. + + + +--- +--- + + + + +# Manual NixOS Configuration (Framework 12, Intel Core Series 3) + +This provides the manual configuration steps for the [NixOS on the Framework Laptop 12 Guide](https://guides.frame.work/Guide/NixOS+on+the+Framework+Laptop+12/412?lang=en) + +There is no nixos-hardware profile for the Framework 12 (Intel Core Series 3) at this time. Unlike the 13th Gen model, which has ``, there is no equivalent entry for this model yet, so the hardware is configured by hand as shown below. + +## Update first + +Update before applying the config below. With new hardware, always best to be as current as possible. + +```bash +sudo nix-channel --update +sudo nixos-rebuild switch +``` + +## Channel-based (default from graphical installer) + +```nix +# /etc/nixos/configuration.nix +imports = [ + ./hardware-configuration.nix +]; +``` + +```bash +sudo nixos-rebuild switch +``` + + +## Enabling the accelerometer and tablet mode +Tablet mode signals the desktop environment that the keyboard is folded back. libinput disables the keyboard and touchpad - firmware also does that. And GNOME/KDE enable screen rotation based on the accelerometer, see below. + +Enable the following: + +```nix +# Framework 12 (Dahlia) tablet mode + rotation + boot.initrd.kernelModules = [ "pinctrl_intel_platform" ]; + + boot.extraModprobeConfig = '' + softdep soc_button_array pre: pinctrl_intel_platform + ''; + +# Screen auto-rotation (accelerometer) + hardware.sensor.iio.enable = true; + +# On-screen keyboard (KDE Plasma) + sensor CLI tool + environment.systemPackages = with pkgs; [ + kdePackages.plasma-keyboard + iio-sensor-proxy + ]; +``` + +`pinctrl_intel_platform` brings up the pin controller early, and the `softdep` loads it before `soc_button_array` so mode detection has its pins at boot. `plasma-keyboard` is the KDE Plasma on-screen keyboard, so this applies to Plasma sessions; GNOME ships its own. `iio-sensor-proxy` provides the sensor service and the `monitor-sensor` CLI for testing rotation. diff --git a/framework12/openSUSE-Tumbleweed.md b/framework12/openSUSE-Tumbleweed.md new file mode 100644 index 0000000..fb7f227 --- /dev/null +++ b/framework12/openSUSE-Tumbleweed.md @@ -0,0 +1,246 @@ +# Tablet Mode and Screen Rotation on openSUSE Tumbleweed (Framework Laptop 12) + +This guide enables the full tablet experience on the Framework Laptop 12 running +openSUSE Tumbleweed: automatic screen rotation, keyboard/touchpad disabling when +folded, and an on-screen keyboard. + +Tablet mode is made up of three independent pieces: + +1. **Tablet-mode switch detection** — folding the screen back fires a + `SW_TABLET_MODE` event. libinput (and the firmware) then disables the + keyboard and touchpad. +2. **Automatic screen rotation** — `iio-sensor-proxy` reads the accelerometer + and passes orientation to the desktop over D-Bus; the compositor rotates the + display and remaps touch/pen input. +3. **Touch UI / on-screen keyboard** — provided by the desktop environment. + +The kernel and sensor plumbing is the same regardless of desktop. The rotation +and on-screen keyboard behaviour differs between GNOME and KDE, and between X11 +and Wayland — see the desktop sections below. + +> **Short version for KDE users:** install `iio-sensor-proxy` and +> `maliit-keyboard`, **log in to the Plasma (Wayland) session**, select Maliit +> as the virtual keyboard, and set Wayland as your default. X11 will not +> auto-rotate. + +--- + +## 1. Kernel modules + +Tablet-mode detection and the accelerometer depend on two modules: + +``` +lsmod | grep -E 'pinctrl_tigerlake|soc_button_array' +``` + +Expected output (the counts may differ): + +``` +soc_button_array 24576 0 +pinctrl_tigerlake 28672 5 +``` + +On openSUSE Tumbleweed both are built as loadable modules and autoload, so this +normally needs no action. If `pinctrl_tigerlake` is missing, force it to load: + +``` +echo pinctrl_tigerlake | sudo tee /etc/modules-load.d/framework12.conf +``` + +--- + +## 2. Install iio-sensor-proxy + +``` +sudo zypper install iio-sensor-proxy +rpm -q iio-sensor-proxy +``` + +Version **3.7** has a bug that prevents it from delivering accelerometer events +to userspace (GNOME, KDE, …). Tumbleweed currently ships **3.8 or newer**, so +the bug does not affect you and **no workaround is required**. + +Only if `monitor-sensor` (next step) shows the proxy appearing but orientation +never changing, apply the udev workaround that comments out the problematic rule: + +``` +sed 's/.*iio-buffer-accel/#&/' /usr/lib/udev/rules.d/80-iio-sensor-proxy.rules \ + | sudo tee /etc/udev/rules.d/80-iio-sensor-proxy.rules +sudo udevadm trigger --settle +sudo systemctl restart iio-sensor-proxy +``` + +--- + +## 3. Verify the accelerometer + +``` +monitor-sensor --accel +``` + +Tilt the laptop from side to side. You should see orientation and tilt lines +appear: + +``` + Waiting for iio-sensor-proxy to appear ++++ iio-sensor-proxy appeared +=== Has accelerometer (orientation: normal) + Accelerometer orientation changed: normal + Tilt changed: vertical +``` + +If orientation changes as you move the laptop, the sensor stack is working. + +--- + +## 4. Verify the tablet-mode switch + +``` +sudo libinput debug-events +``` + +Fold the screen back into tablet posture, then forward again. You should see the +switch toggle on the `gpio-keys` device: + +``` + event2 SWITCH_TOGGLE switch tablet-mode state 1 # folded into tablet + event2 SWITCH_TOGGLE switch tablet-mode state 0 # folded back to laptop +``` + +`state 1` is what triggers keyboard/touchpad disabling and, in tablet-aware +desktops, the touch UI. + +--- + +## 5. Desktop environment + +### KDE Plasma + +**Use the Wayland session.** Automatic rotation and the tablet UI are +Wayland-only features in Plasma. KWin's X11 backend was feature-frozen years ago +and never gained accelerometer-driven rotation, so on **X11 nothing will rotate** +no matter how healthy the sensor stack is. + +Log out and pick **Plasma (Wayland)** at the SDDM session selector. Rotation then +follows the accelerometer automatically and the touchscreen/stylus are remapped +to match. Rotation options live in **System Settings → Display & Monitor**, +including "rotate only in tablet mode", which uses the switch from step 4. + +#### On-screen keyboard (Plasma Wayland) + +Plasma ships with no virtual-keyboard backend selected, so nothing pops up by +default. Install Maliit: + +``` +sudo zypper install maliit-keyboard +``` + +Then **System Settings → Keyboard → Virtual Keyboard** (labelled "Screen +keyboard" on Plasma 6.6+) → select **Maliit Keyboard**. Log out and back in. + +Notes: + +- The keyboard only auto-appears when a text field is focused **and** the device + is in tablet posture (`tablet-mode state 1`). In laptop mode it stays hidden. +- There is also a manual toggle in the system tray. +- If the dropdown is empty after installing Maliit, relog (or reboot) so the + input-method plugin registers. +- Plasma 6.6+ also has a native "Plasma Keyboard" option in the same dropdown; + Maliit remains the more battle-tested choice on convertibles. + +#### Stylus / pen (Plasma Wayland) + +The Framework 12's built-in stylus is configured in the native Wayland Drawing +Tablet module: **System Settings → Drawing Tablet (Zeichentablett) → Pen +(Stift)**. (This is a Wayland-only KCM; on X11 you'd need the older +`wacomtablet` module instead — another reason to stay on Wayland.) + +Useful options on the **Pen/Stift** page: + +- **Button mapping** — reassign each stylus button to a mouse click + (right/middle), a key combination or modifier, or disable it entirely. +- **Pressure curve** — adjust the curve that maps physical pen pressure to + logical pressure, for apps like Krita; you can also limit the usable pressure + range. +- **Tap to execute** — when enabled, a button action only fires while the pen + tip is touching the screen; disable it to allow actions while hovering. + +Related controls live alongside the Pen page in the same module: + +- **Tablet tester** — shows live pressure and tilt so you can confirm the pen + is working as expected. +- **Calibration** — corrects parallax on the built-in display; worth running + once on the Framework 12 so the cursor lands exactly under the pen tip. +- **Screen mapping / orientation** — keep the pen mapped to the internal + display; orientation follows auto-rotate, so you normally leave this alone. + +Power users can script the same settings from the command line with +`ktabletconfig`. + +### GNOME + +GNOME on Wayland auto-rotates and shows its on-screen keyboard once +`iio-sensor-proxy` is delivering events — no extra packages needed. + +--- + +## 6. Make Wayland the default session + +openSUSE ships patches that bias the default/auto-login session toward Plasma +**X11** (the `default.desktop` symlink under `/usr/share/xsessions/` points at an +X11 session). SDDM otherwise remembers the **last session you logged into, per +user**, so simply logging into Wayland usually makes it your default going +forward. + +Confirm which session you are in: + +``` +echo $XDG_SESSION_TYPE # should print: wayland +``` + +If you use **autologin** and it keeps reverting to X11, pin the session. First +check the exact session file names (they vary by packaging): + +``` +ls /usr/share/wayland-sessions/ /usr/share/xsessions/ +``` + +On current Tumbleweed the Wayland session is `plasmawayland.desktop`. Set it via +the GUI — **System Settings → Startup and Shutdown → Login Screen (SDDM) → +Behavior** → "Automatically log in" → session = **Plasma (Wayland)** — or with a +config file `/etc/sddm.conf.d/10-session.conf`: + +``` +[Autologin] +User=YOUR_USERNAME +Session=plasmawayland.desktop +``` + +Avoid repointing the `default.desktop` symlink by hand; package updates will +overwrite it. + +--- + +## Known issues + +- **Keyboard/touchpad not re-enabling when leaving tablet mode.** Some Framework + 12 units do not restore the keyboard and touchpad after folding back to laptop + posture. Keep the BIOS current via `fwupd`/LVFS, as this has been partly + addressed in firmware. +- **X11 + KDE will not auto-rotate.** This is by design (KWin X11 is + feature-frozen) and cannot be fixed in configuration. Use Wayland, or a + third-party rotation daemon such as `rot8` if you must stay on X11. + +--- + +## Quick reference + +| Component | Package / location | Check | +|----------------------|------------------------------------------|-----------------------------------------| +| Kernel modules | `pinctrl_tigerlake`, `soc_button_array` | `lsmod \| grep -E 'pinctrl_tigerlake\|soc_button_array'` | +| Accelerometer daemon | `iio-sensor-proxy` (≥ 3.8) | `monitor-sensor --accel` | +| Tablet switch | kernel / `gpio-keys` | `sudo libinput debug-events` | +| Rotation | Plasma **Wayland** / GNOME Wayland | rotate the device | +| On-screen keyboard | `maliit-keyboard` | focus a text field in tablet mode | +| Stylus / pen | Drawing Tablet KCM (**Wayland**) | System Settings → Zeichentablett → Stift | +| Default session | SDDM last-session / autologin config | `echo $XDG_SESSION_TYPE` | diff --git a/framework12/stylus.md b/framework12/stylus.md new file mode 100644 index 0000000..843dc2d --- /dev/null +++ b/framework12/stylus.md @@ -0,0 +1,90 @@ +# Framework 12 - Stylus + +The touchscreen and stylus do not need any special drivers on Windows or Linux. +It is an I2C HID device, just like the touchpad and generic drivers can enable full functionality. +On Windows there is a custom driver to enable wake by touch. + +## Protocols + +The touchscreen supports MPP and USI protocols, which were developed for Windows and ChromeOS, respectively. +In practice, both protocols work on both operating systems. So just enable, whatever your stylus supports. + +The Framework Stylus supports both protocols, so the BIOS setting can be set to either. +Both protocols support the same functionality, with the difference that USI supports battery and firmware version reporting. MPP relies on Bluetooth to do the same, which the Framework stylus does not support. + +## Stylus functionality + +- Pressure Level 0.00 to 1.00 +- X and Y tilt 60 degree to any direction +- Tap + - Usually interpreted as left-click +- Lower Button (Close to the tip) + - Eraser +- Upper Button (Closer to your hand) + - `BTN_STYLUS` - Usually interpreted as right-click + +Note that application usually interpret the buttons (especially eraser) differently if the stylus is touching the screen versus hovering. +For example, drawing applications will temporarily switch to the eraser tool when hovering and pressing the eraser button, but only erase once the stylus also touches the screen. + +### Remapping the buttons + +The buttons are sent as described in other sections by the kernel. Applications can interpret them as they wish. + +## Testing on Linux + +To test, if the stylus works correctly and reports all data as expected, run +`sudo libinput debug-events` and use the stylus in a different window. + +The reported data looks the same, no matter what protocol is selected. +However it depends very much on what the stylus reports! +Below data is from the Framework Stylus. + + +``` + Stylus enters detection range of the display + | + event9 TABLET_TOOL_PROXIMITY +267.547s 247.63*/138.28* tilt: 13.57*/12.67* pressure: 0.00* pen (0, id 0x4538) proximity-in axes:pt btn:SS2 + + + Y Coordinate + | + X Coordinate | X/Y Tilt (-60 to +60) Pressure (0.00 to 1.00) + | | | | | + event9 TABLET_TOOL_AXIS 263 +35.807s 209.39*/105.86* tilt: 6.43 /1.77 pressure: 0.00 + + + Stylus exits detection range of the display + | + event9 TABLET_TOOL_PROXIMITY +267.620s 248.27 /137.19 tilt: 12.87 /9.80 pressure: 0.00 pen (0, id 0x4538) proximity-out + + Stylus pressed on the screen/Is removed from the screen + | + event9 TABLET_TOOL_TIP +329.857s 238.76*/144.14* tilt: 12.92 /10.49 pressure: 0.09* down + event9 TABLET_TOOL_TIP +330.011s 238.11*/144.17* tilt: 12.02*/10.32* pressure: 0.00* up + + + Lower (eraser) button is pressed + | + event9 TABLET_TOOL_PROXIMITY +403.162s 240.76 /141.36 tilt: 50.05 /-1.43 pressure: 0.00 pen (0, id 0x4538) proximity-out + event9 TABLET_TOOL_PROXIMITY +403.165s 240.79*/141.21* tilt: 49.95*/-1.30* pressure: 0.00* eraser (0, id 0xd278) proximity-in axes:pt btn:SS2 + + Lower (eraser) button is released + | + event9 TABLET_TOOL_PROXIMITY +403.177s 240.79 /141.03 tilt: 49.95 /-1.30 pressure: 0.00 eraser (0, id 0xd278) proximity-out + event9 TABLET_TOOL_PROXIMITY +403.180s 240.76*/140.86* tilt: 51.11*/-0.83* pressure: 0.00* pen (0, id 0x4538) proximity-in axes:pt btn:SS2 + + Upper button is pressed + | + event9 TABLET_TOOL_BUTTON +8.646s s331 (BTN_STYLUS) released, seat count: 0 + + Upper button is released + | + event9 TABLET_TOOL_BUTTON +8.360s s331 (BTN_STYLUS) pressed, seat count: 1 +``` + +### Firmware Details with USI + +See: + +- [Stylus IDs and Firmware Version](https://github.com/FrameworkComputer/framework-system/blob/main/EXAMPLES.md#stylus-framework-12) +- [Stylus Battery Level](https://github.com/FrameworkComputer/framework-system/blob/main/EXAMPLES.md#stylus-framework-12-1) diff --git a/framework13/FW-13-Pro-Fedora-all-Intel-Core-Ultra-Series-3.md b/framework13/FW-13-Pro-Fedora-all-Intel-Core-Ultra-Series-3.md new file mode 100644 index 0000000..a3699c5 --- /dev/null +++ b/framework13/FW-13-Pro-Fedora-all-Intel-Core-Ultra-Series-3.md @@ -0,0 +1,74 @@ +# This is for Intel® Core™ Ultra Series 3 Framework Laptop 13 Pro ONLY. + +## This will: + +- Getting your laptop fully updated. +- Enable improved fractional scaling support Fedora's GNOME environment using Wayland. +- Enabling tap to click on the touchpad. + +  +  +  + + +### Step 1 Updating your software packages + +- Browse to the horizontal line in the upper left corner, click to open it. +- Type out the word terminal, click to open it. +- Copy the code below in the gray box, right click/paste it into the terminal window. +- Then press the enter key, user password, enter key, **reboot.** + + +``` +sudo dnf upgrade +``` +> **TIP:** You can use the little clipboard icon to the right of the code to copy to your clipboard. + + +**Reboot** + +  +  +  + +### Step 2 - If you want to enable fractional scaling on Wayland: + +- Type out the word Displays. +- Look for scale you want and select it, click Apply. + +  +  +  + +### Step 3 - If you want to enable "tap-to-click" on the touchpad: + +- Browse to the horizontal line in the upper left corner, click to open it. +- Type out the word mouse, look for Mouse and Touchpad, click to open it. +- Click the touchpad option at the top. +- Under "Clicking", select Tap to Click and enable it. + +  +  +  +### Bonus Step (for former Mac users) Reduce Font Scaling to Match Your Needs + +We received feedback that for users coming from OS X, installing GNOME Tweaks, browsing to Fonts, and reducing the font size from 1.00 to 0.80 may be preferred. + +- Goto Displays, set scaling to 200%. This will look too large, so let's fix the fonts. +- Install with: + +``` +sudo dnf install gnome-tweaks -y +``` + +- Open Tweaks by using the "Super" or Windows key, search tweaks, and enter. + +- At the top, select fonts. Now in that panel, scroll all the way down. Look for Size. Change from 1.00 to 0.80. Close Tweaks. + + Note: This is for the displays for the laptop only. This will look super odd on external displays and likely too large even still. + + + +  +  +  diff --git a/framework13/FW-13-Pro-NixOS-all-Intel-Core-Ultra-Series-3.md b/framework13/FW-13-Pro-NixOS-all-Intel-Core-Ultra-Series-3.md new file mode 100644 index 0000000..f0d4922 --- /dev/null +++ b/framework13/FW-13-Pro-NixOS-all-Intel-Core-Ultra-Series-3.md @@ -0,0 +1,40 @@ +# Adding the NixOS-Hardware Module (Framework 13 Pro, Intel Core Ultra Series 3) +This provides the hardware channel steps for the [NixOS on the Framework Laptop 13 Pro Guide](https://guides.frame.work/Guide/NixOS+on+the+Framework+Laptop+13+Pro/780?lang=en) + +## Channel-based (default from graphical installer) + +```bash +sudo nix-channel --add https://github.com/NixOS/nixos-hardware/archive/master.tar.gz nixos-hardware +sudo nix-channel --update +``` + +```nix +# /etc/nixos/configuration.nix +imports = [ + ./hardware-configuration.nix + +]; +``` + +```bash +sudo nixos-rebuild switch +``` + +## Flake-Based + +```nix +# flake.nix inputs +inputs.nixos-hardware.url = "github:NixOS/nixos-hardware/master"; +``` + +```nix +# flake.nix modules list +modules = [ + ./configuration.nix + nixos-hardware.nixosModules.framework-intel-core-ultra-series3 +]; +``` + +```bash +sudo nixos-rebuild switch --flake .# +``` diff --git a/framework13/Fedora-all-11thGen.md b/framework13/Fedora-all-11thGen.md new file mode 100644 index 0000000..5acf460 --- /dev/null +++ b/framework13/Fedora-all-11thGen.md @@ -0,0 +1,74 @@ +# This is for 11th Gen Intel® Core™ Framework Laptop 13 ONLY + +## This will: + +- Getting your laptop fully updated. +- Enable improved fractional scaling support Fedora's GNOME environment using Wayland. +- Enabling tap to click on the touchpad. + +  +  +  + + +### Step 1 Updating your software packages + +- Browse to the horizontal line in the upper left corner, click to open it. +- Type out the word terminal, click to open it. +- Copy the code below in the gray box, right click/paste it into the terminal window. +- Then press the enter key, user password, enter key, **reboot.** + + +``` +sudo dnf upgrade +``` +> **TIP:** You can use the little clipboard icon to the right of the code to copy to your clipboard. + + +**Reboot** + +  +  +  + +### Step 2 - If you want to enable fractional scaling on Wayland: + +- Type out the word Displays. +- Look for scale you want and select it, click Apply. + +  +  +  + +### Step 3 - If you want to enable "tap-to-click" on the touchpad: + +- Browse to the horizontal line in the upper left corner, click to open it. +- Type out the word mouse, look for Mouse and Touchpad, click to open it. +- Click the touchpad option at the top. +- Under "Clicking", select Tap to Click and enable it. + +  +  +  +### Bonus Step (for former Mac users) Reduce Font Scaling to Match Your Needs + +We received feedback that for users coming from OS X, installing GNOME Tweaks, browsing to Fonts, and reducing the font size from 1.00 to 0.80 may be preferred. + +- Goto Displays, set scaling to 200%. This will look too large, so let's fix the fonts. +- Install with: + +``` +sudo dnf install gnome-tweaks -y +``` + +- Open Tweaks by using the "Super" or Windows key, search tweaks, and enter. + +- At the top, select fonts. Now in that panel, scroll all the way down. Look for Size. Change from 1.00 to 0.80. Close Tweaks. + + Note: This is for the displays for the laptop only. This will look super odd on external displays and likely too large even still. + + + +  +  +  diff --git a/framework13/Fedora-all-12thGen.md b/framework13/Fedora-all-12thGen.md new file mode 100644 index 0000000..31f9849 --- /dev/null +++ b/framework13/Fedora-all-12thGen.md @@ -0,0 +1,74 @@ +# This is for 12th Gen Intel® Core™ Framework Laptop 13 ONLY + +## This will: + +- Getting your laptop fully updated. +- Enable improved fractional scaling support Fedora's GNOME environment using Wayland. +- Enabling tap to click on the touchpad. + +  +  +  + + +### Step 1 Updating your software packages + +- Browse to the horizontal line in the upper left corner, click to open it. +- Type out the word terminal, click to open it. +- Copy the code below in the gray box, right click/paste it into the terminal window. +- Then press the enter key, user password, enter key, **reboot.** + + +``` +sudo dnf upgrade +``` +> **TIP:** You can use the little clipboard icon to the right of the code to copy to your clipboard. + + +**Reboot** + +  +  +  + +### Step 2 - If you want to enable fractional scaling on Wayland: + +- Type out the word Displays. +- Look for scale you want and select it, click Apply. + +  +  +  + +### Step 3 - If you want to enable "tap-to-click" on the touchpad: + +- Browse to the horizontal line in the upper left corner, click to open it. +- Type out the word mouse, look for Mouse and Touchpad, click to open it. +- Click the touchpad option at the top. +- Under "Clicking", select Tap to Click and enable it. + +  +  +  +### Bonus Step (for former Mac users) Reduce Font Scaling to Match Your Needs + +We received feedback that for users coming from OS X, installing GNOME Tweaks, browsing to Fonts, and reducing the font size from 1.00 to 0.80 may be preferred. + +- Goto Displays, set scaling to 200%. This will look too large, so let's fix the fonts. +- Install with: + +``` +sudo dnf install gnome-tweaks -y +``` + +- Open Tweaks by using the "Super" or Windows key, search tweaks, and enter. + +- At the top, select fonts. Now in that panel, scroll all the way down. Look for Size. Change from 1.00 to 0.80. Close Tweaks. + + Note: This is for the displays for the laptop only. This will look super odd on external displays and likely too large even still. + + + +  +  +  diff --git a/framework13/Fedora-all-13thGen.md b/framework13/Fedora-all-13thGen.md new file mode 100644 index 0000000..e98e9dc --- /dev/null +++ b/framework13/Fedora-all-13thGen.md @@ -0,0 +1,74 @@ +# This is for 13th Gen Intel® Core™ Framework Laptop 13 ONLY + +## This will: + +- Getting your laptop fully updated. +- Enable improved fractional scaling support Fedora's GNOME environment using Wayland. +- Enabling tap to click on the touchpad. + +  +  +  + + +### Step 1 Updating your software packages + +- Browse to the horizontal line in the upper left corner, click to open it. +- Type out the word terminal, click to open it. +- Copy the code below in the gray box, right click/paste it into the terminal window. +- Then press the enter key, user password, enter key, **reboot.** + + +``` +sudo dnf upgrade +``` +> **TIP:** You can use the little clipboard icon to the right of the code to copy to your clipboard. + + +**Reboot** + +  +  +  + +### Step 2 - If you want to enable fractional scaling on Wayland: + +- Type out the word Displays. +- Look for scale you want and select it, click Apply. + +  +  +  + +### Step 3 - If you want to enable "tap-to-click" on the touchpad: + +- Browse to the horizontal line in the upper left corner, click to open it. +- Type out the word mouse, look for Mouse and Touchpad, click to open it. +- Click the touchpad option at the top. +- Under "Clicking", select Tap to Click and enable it. + +  +  +  +### Bonus Step (for former Mac users) Reduce Font Scaling to Match Your Needs + +We received feedback that for users coming from OS X, installing GNOME Tweaks, browsing to Fonts, and reducing the font size from 1.00 to 0.80 may be preferred. + +- Goto Displays, set scaling to 200%. This will look too large, so let's fix the fonts. +- Install with: + +``` +sudo dnf install gnome-tweaks -y +``` + +- Open Tweaks by using the "Super" or Windows key, search tweaks, and enter. + +- At the top, select fonts. Now in that panel, scroll all the way down. Look for Size. Change from 1.00 to 0.80. Close Tweaks. + + Note: This is for the displays for the laptop only. This will look super odd on external displays and likely too large even still. + + + +  +  +  diff --git a/framework13/Fedora-all-Intel-Core-Ultra-Series-1.md b/framework13/Fedora-all-Intel-Core-Ultra-Series-1.md new file mode 100644 index 0000000..0aa64aa --- /dev/null +++ b/framework13/Fedora-all-Intel-Core-Ultra-Series-1.md @@ -0,0 +1,74 @@ +# This is for Intel® Core™ Ultra Series 1 Framework Laptop 13 ONLY. + +## This will: + +- Getting your laptop fully updated. +- Enable improved fractional scaling support Fedora's GNOME environment using Wayland. +- Enabling tap to click on the touchpad. + +  +  +  + + +### Step 1 Updating your software packages + +- Browse to the horizontal line in the upper left corner, click to open it. +- Type out the word terminal, click to open it. +- Copy the code below in the gray box, right click/paste it into the terminal window. +- Then press the enter key, user password, enter key, **reboot.** + + +``` +sudo dnf upgrade +``` +> **TIP:** You can use the little clipboard icon to the right of the code to copy to your clipboard. + + +**Reboot** + +  +  +  + +### Step 2 - If you want to enable fractional scaling on Wayland: + +- Type out the word Displays. +- Look for scale you want and select it, click Apply. + +  +  +  + +### Step 3 - If you want to enable "tap-to-click" on the touchpad: + +- Browse to the horizontal line in the upper left corner, click to open it. +- Type out the word mouse, look for Mouse and Touchpad, click to open it. +- Click the touchpad option at the top. +- Under "Clicking", select Tap to Click and enable it. + +  +  +  +### Bonus Step (for former Mac users) Reduce Font Scaling to Match Your Needs + +We received feedback that for users coming from OS X, installing GNOME Tweaks, browsing to Fonts, and reducing the font size from 1.00 to 0.80 may be preferred. + +- Goto Displays, set scaling to 200%. This will look too large, so let's fix the fonts. +- Install with: + +``` +sudo dnf install gnome-tweaks -y +``` + +- Open Tweaks by using the "Super" or Windows key, search tweaks, and enter. + +- At the top, select fonts. Now in that panel, scroll all the way down. Look for Size. Change from 1.00 to 0.80. Close Tweaks. + + Note: This is for the displays for the laptop only. This will look super odd on external displays and likely too large even still. + + + +  +  +  diff --git a/framework13/Fedora-all-amd7040-fw13.md b/framework13/Fedora-all-amd7040-fw13.md new file mode 100644 index 0000000..d058a22 --- /dev/null +++ b/framework13/Fedora-all-amd7040-fw13.md @@ -0,0 +1,72 @@ +# This is for AMD Ryzen 7040 Series configuration on the Framework Laptop 13 ONLY. + +## This will: + +- Get your laptop fully updated. +- Enable improved fractional scaling support Fedora's GNOME environment using Wayland. +- Enabling tap to click on the touchpad. + +  +  +  + +### Step 1 Updating your software packages + +- Browse to the horizontal line in the upper left corner, click to open it. +- Type out the word terminal, click to open it. +- Copy the code below in the gray box, right click/paste it into the terminal window. +- Then press the ⏎ Enter key, user password, ⏎ Enter key, **reboot.** + + +``` +sudo dnf upgrade +``` +> **TIP:** You can use the little clipboard icon to the right of the code to copy to your clipboard. + + +**Reboot** + +  +  +  + +### Step 2 - If you want to enable fractional scaling on Wayland: + +- Browse to the horizontal line in the upper left corner, click to open it. +- Type out the word Displays. +- Look for scale you want and select it, click Apply. + +  +  +  +### Step 3 - If you want to enable "tap-to-click" on the touchpad: + +- Browse to the horizontal line in the upper left corner, click to open it. +- Type out the word mouse, look for Mouse and Touchpad, click to open it. +- Click the touchpad option at the top. +- Under "Clicking", select Tap to Click and enable it. + +  +  +  + +### Bonus Step (for former Mac users) Reduce Font Scaling to Match Your Needs + +We received feedback that for users coming from OS X, installing GNOME Tweaks, browsing to Fonts, and reducing the font size from 1.00 to 0.80 may be preferred. + +- Go to Displays, set scaling to 200%. This will look too large, so let's fix the fonts. +- Install with: + +``` +sudo dnf install gnome-tweaks -y +``` + +- Open Tweaks by using the Super or ⊞ Win key, search tweaks, and ⏎ Enter. + +- At the top, select fonts. Now in that panel, scroll all the way down. Look for Size. Change from 1.00 to 0.80. Close Tweaks. + + Note: This is for the displays for the laptop only. This will look super odd on external displays and likely too large even still. + +  +  +  diff --git a/framework13/Fedora41-11thGen.md b/framework13/Fedora41-11thGen.md new file mode 100644 index 0000000..5acf460 --- /dev/null +++ b/framework13/Fedora41-11thGen.md @@ -0,0 +1,74 @@ +# This is for 11th Gen Intel® Core™ Framework Laptop 13 ONLY + +## This will: + +- Getting your laptop fully updated. +- Enable improved fractional scaling support Fedora's GNOME environment using Wayland. +- Enabling tap to click on the touchpad. + +  +  +  + + +### Step 1 Updating your software packages + +- Browse to the horizontal line in the upper left corner, click to open it. +- Type out the word terminal, click to open it. +- Copy the code below in the gray box, right click/paste it into the terminal window. +- Then press the enter key, user password, enter key, **reboot.** + + +``` +sudo dnf upgrade +``` +> **TIP:** You can use the little clipboard icon to the right of the code to copy to your clipboard. + + +**Reboot** + +  +  +  + +### Step 2 - If you want to enable fractional scaling on Wayland: + +- Type out the word Displays. +- Look for scale you want and select it, click Apply. + +  +  +  + +### Step 3 - If you want to enable "tap-to-click" on the touchpad: + +- Browse to the horizontal line in the upper left corner, click to open it. +- Type out the word mouse, look for Mouse and Touchpad, click to open it. +- Click the touchpad option at the top. +- Under "Clicking", select Tap to Click and enable it. + +  +  +  +### Bonus Step (for former Mac users) Reduce Font Scaling to Match Your Needs + +We received feedback that for users coming from OS X, installing GNOME Tweaks, browsing to Fonts, and reducing the font size from 1.00 to 0.80 may be preferred. + +- Goto Displays, set scaling to 200%. This will look too large, so let's fix the fonts. +- Install with: + +``` +sudo dnf install gnome-tweaks -y +``` + +- Open Tweaks by using the "Super" or Windows key, search tweaks, and enter. + +- At the top, select fonts. Now in that panel, scroll all the way down. Look for Size. Change from 1.00 to 0.80. Close Tweaks. + + Note: This is for the displays for the laptop only. This will look super odd on external displays and likely too large even still. + + + +  +  +  diff --git a/framework13/Fedora41-12thGen.md b/framework13/Fedora41-12thGen.md new file mode 100644 index 0000000..31f9849 --- /dev/null +++ b/framework13/Fedora41-12thGen.md @@ -0,0 +1,74 @@ +# This is for 12th Gen Intel® Core™ Framework Laptop 13 ONLY + +## This will: + +- Getting your laptop fully updated. +- Enable improved fractional scaling support Fedora's GNOME environment using Wayland. +- Enabling tap to click on the touchpad. + +  +  +  + + +### Step 1 Updating your software packages + +- Browse to the horizontal line in the upper left corner, click to open it. +- Type out the word terminal, click to open it. +- Copy the code below in the gray box, right click/paste it into the terminal window. +- Then press the enter key, user password, enter key, **reboot.** + + +``` +sudo dnf upgrade +``` +> **TIP:** You can use the little clipboard icon to the right of the code to copy to your clipboard. + + +**Reboot** + +  +  +  + +### Step 2 - If you want to enable fractional scaling on Wayland: + +- Type out the word Displays. +- Look for scale you want and select it, click Apply. + +  +  +  + +### Step 3 - If you want to enable "tap-to-click" on the touchpad: + +- Browse to the horizontal line in the upper left corner, click to open it. +- Type out the word mouse, look for Mouse and Touchpad, click to open it. +- Click the touchpad option at the top. +- Under "Clicking", select Tap to Click and enable it. + +  +  +  +### Bonus Step (for former Mac users) Reduce Font Scaling to Match Your Needs + +We received feedback that for users coming from OS X, installing GNOME Tweaks, browsing to Fonts, and reducing the font size from 1.00 to 0.80 may be preferred. + +- Goto Displays, set scaling to 200%. This will look too large, so let's fix the fonts. +- Install with: + +``` +sudo dnf install gnome-tweaks -y +``` + +- Open Tweaks by using the "Super" or Windows key, search tweaks, and enter. + +- At the top, select fonts. Now in that panel, scroll all the way down. Look for Size. Change from 1.00 to 0.80. Close Tweaks. + + Note: This is for the displays for the laptop only. This will look super odd on external displays and likely too large even still. + + + +  +  +  diff --git a/framework13/Fedora41-13thGen.md b/framework13/Fedora41-13thGen.md new file mode 100644 index 0000000..e98e9dc --- /dev/null +++ b/framework13/Fedora41-13thGen.md @@ -0,0 +1,74 @@ +# This is for 13th Gen Intel® Core™ Framework Laptop 13 ONLY + +## This will: + +- Getting your laptop fully updated. +- Enable improved fractional scaling support Fedora's GNOME environment using Wayland. +- Enabling tap to click on the touchpad. + +  +  +  + + +### Step 1 Updating your software packages + +- Browse to the horizontal line in the upper left corner, click to open it. +- Type out the word terminal, click to open it. +- Copy the code below in the gray box, right click/paste it into the terminal window. +- Then press the enter key, user password, enter key, **reboot.** + + +``` +sudo dnf upgrade +``` +> **TIP:** You can use the little clipboard icon to the right of the code to copy to your clipboard. + + +**Reboot** + +  +  +  + +### Step 2 - If you want to enable fractional scaling on Wayland: + +- Type out the word Displays. +- Look for scale you want and select it, click Apply. + +  +  +  + +### Step 3 - If you want to enable "tap-to-click" on the touchpad: + +- Browse to the horizontal line in the upper left corner, click to open it. +- Type out the word mouse, look for Mouse and Touchpad, click to open it. +- Click the touchpad option at the top. +- Under "Clicking", select Tap to Click and enable it. + +  +  +  +### Bonus Step (for former Mac users) Reduce Font Scaling to Match Your Needs + +We received feedback that for users coming from OS X, installing GNOME Tweaks, browsing to Fonts, and reducing the font size from 1.00 to 0.80 may be preferred. + +- Goto Displays, set scaling to 200%. This will look too large, so let's fix the fonts. +- Install with: + +``` +sudo dnf install gnome-tweaks -y +``` + +- Open Tweaks by using the "Super" or Windows key, search tweaks, and enter. + +- At the top, select fonts. Now in that panel, scroll all the way down. Look for Size. Change from 1.00 to 0.80. Close Tweaks. + + Note: This is for the displays for the laptop only. This will look super odd on external displays and likely too large even still. + + + +  +  +  diff --git a/framework13/Fedora41-Setup-Intel-Core-Ultra-Series-1.md b/framework13/Fedora41-Setup-Intel-Core-Ultra-Series-1.md new file mode 100644 index 0000000..0aa64aa --- /dev/null +++ b/framework13/Fedora41-Setup-Intel-Core-Ultra-Series-1.md @@ -0,0 +1,74 @@ +# This is for Intel® Core™ Ultra Series 1 Framework Laptop 13 ONLY. + +## This will: + +- Getting your laptop fully updated. +- Enable improved fractional scaling support Fedora's GNOME environment using Wayland. +- Enabling tap to click on the touchpad. + +  +  +  + + +### Step 1 Updating your software packages + +- Browse to the horizontal line in the upper left corner, click to open it. +- Type out the word terminal, click to open it. +- Copy the code below in the gray box, right click/paste it into the terminal window. +- Then press the enter key, user password, enter key, **reboot.** + + +``` +sudo dnf upgrade +``` +> **TIP:** You can use the little clipboard icon to the right of the code to copy to your clipboard. + + +**Reboot** + +  +  +  + +### Step 2 - If you want to enable fractional scaling on Wayland: + +- Type out the word Displays. +- Look for scale you want and select it, click Apply. + +  +  +  + +### Step 3 - If you want to enable "tap-to-click" on the touchpad: + +- Browse to the horizontal line in the upper left corner, click to open it. +- Type out the word mouse, look for Mouse and Touchpad, click to open it. +- Click the touchpad option at the top. +- Under "Clicking", select Tap to Click and enable it. + +  +  +  +### Bonus Step (for former Mac users) Reduce Font Scaling to Match Your Needs + +We received feedback that for users coming from OS X, installing GNOME Tweaks, browsing to Fonts, and reducing the font size from 1.00 to 0.80 may be preferred. + +- Goto Displays, set scaling to 200%. This will look too large, so let's fix the fonts. +- Install with: + +``` +sudo dnf install gnome-tweaks -y +``` + +- Open Tweaks by using the "Super" or Windows key, search tweaks, and enter. + +- At the top, select fonts. Now in that panel, scroll all the way down. Look for Size. Change from 1.00 to 0.80. Close Tweaks. + + Note: This is for the displays for the laptop only. This will look super odd on external displays and likely too large even still. + + + +  +  +  diff --git a/framework13/Fedora41-amd-fw13.md b/framework13/Fedora41-amd-fw13.md new file mode 100644 index 0000000..9e53efb --- /dev/null +++ b/framework13/Fedora41-amd-fw13.md @@ -0,0 +1,162 @@ +# This is for AMD Ryzen 7040 Series configuration on the Framework Laptop 13 ONLY. + +## This will: + +- Get your laptop fully updated. +- Enable improved fractional scaling support Fedora's GNOME environment using Wayland. +- Enabling tap to click on the touchpad. + +  +  +  + +### Step 1 Updating your software packages + +- Browse to the horizontal line in the upper left corner, click to open it. +- Type out the word terminal, click to open it. +- Copy the code below in the gray box, right click/paste it into the terminal window. +- Then press the ⏎ Enter key, user password, ⏎ Enter key, **reboot.** + + +``` +sudo dnf upgrade +``` +> **TIP:** You can use the little clipboard icon to the right of the code to copy to your clipboard. + + +**Reboot** + +  +  +  + +### Step 2 - If you want to enable fractional scaling on Wayland: + +- Browse to the horizontal line in the upper left corner, click to open it. +- Type out the word Displays. +- Look for scale you want and select it, click Apply. + +  +  +  +### Step 3 - If you want to enable "tap-to-click" on the touchpad: + +- Browse to the horizontal line in the upper left corner, click to open it. +- Type out the word mouse, look for Mouse and Touchpad, click to open it. +- Click the touchpad option at the top. +- Under "Clicking", select Tap to Click and enable it. + +  +  +  + +### Bonus Step (for former Mac users) Reduce Font Scaling to Match Your Needs + +We received feedback that for users coming from OS X, installing GNOME Tweaks, browsing to Fonts, and reducing the font size from 1.00 to 0.80 may be preferred. + +- Go to Displays, set scaling to 200%. This will look too large, so let's fix the fonts. +- Install with: + +``` +sudo dnf install gnome-tweaks -y +``` + +- Open Tweaks by using the Super or ⊞ Win key, search tweaks, and ⏎ Enter. + +- At the top, select fonts. Now in that panel, scroll all the way down. Look for Size. Change from 1.00 to 0.80. Close Tweaks. + + Note: This is for the displays for the laptop only. This will look super odd on external displays and likely too large even still. + +  +  +  + +## Optional and *only if needed* - current AMD Ryzen 7040 Series workarounds to common issues + +### Laggy or stuttering touchpad: +(Customer submitted, not seeing this internally, but if you are, please file a bug so we can get this fixed vs this workaround please) + +- Browse to the horizontal line in the upper left corner, click to open it. +- Type out the word terminal, click to open it. +- Then press the ⏎ Enter key, user password, ⏎ Enter key. + +``` +sudo grubby --update-kernel=ALL --args="amdgpu.dcdebugmask=0x10" +``` +> **TIP:** If you've set other kernel parameters, like from the section above, include both inside `--args=""`. + + +**Reboot** + + +### Buzzing sound from 3.5mm jack + +- Browse to the horizontal line in the upper left corner, click to open it. +- Type out the word terminal, click to open it. +- Copy and paste in the code below (use either the immediate temporary fix or persistent fix). +- Then press the ⏎ Enter key, user password, ⏎ Enter key. + +``` +# Immediate temporary fix to disable power save for running session (no reboot required) +echo 0 | sudo tee /sys/module/snd_hda_intel/parameters/power_save +``` + +``` +# Persistent fix to disable power save using Tuned (either change the power profile or reboot to apply) +# Note: Change "balanced" to the profile you want this set on +sudo mkdir -p /etc/tuned/profiles/balanced/ +sudo cp /usr/lib/tuned/profiles/balanced/tuned.conf /etc/tuned/profiles/balanced/ +sudo sed -i 's/timeout=10/timeout=0/g' /etc/tuned/profiles/balanced/tuned.conf +``` + +> **TIP:** You can use the little clipboard icon to the right of the code to copy to your clipboard. + + +  +  +  + +### 3.5mm jack mic won't work + +- Browse to the horizontal line in the upper left corner, click to open it. +- Type out the word terminal, click to open it. +- Copy and paste in the following code below. +- Press the ⏎ Enter key, user password, ⏎ Enter key. + +``` +sudo tee /etc/modprobe.d/alsa.conf <<< "options snd-hda-intel index=1,0 model=auto,dell-headset-multi" +``` +> **TIP:** You can use the little clipboard icon to the right of the code to copy to your clipboard. + +  +  +  + +------------------------------------------------------------------------------- + +## (No Longer Needed) ~~Optional and *only if needed* - current AMD Ryzen 7040 Series workarounds to common issues~~ + +### ~~To prevent graphical artifacts from appearing:~~ +~~(Note, this workaround may be unneeded as it is difficult to reproduce, however, if you find you're experiencing [the issue described here](https://bugzilla.redhat.com/show_bug.cgi?id=2247154#c3), you can implement this boot parameter)~~ + + +- ~~Browse to the horizontal line in the upper left corner, click to open it.~~ +- ~~Type out the word terminal, click to open it.~~ +- ~~Then press the ⏎ Enter key, user password, ⏎ Enter key.~~ + +``` +sudo grubby --update-kernel=ALL --args="amdgpu.sg_display=0" +``` +> ~~**TIP:** You can use the little clipboard icon to the right of the code to copy to your clipboard.~~ + + +~~**Reboot**~~ + + + + +## ~~MediaTek Bluetooth with s2idle workaround~~ + +- ~~[Simply visit this page](https://github.com/FrameworkComputer/linux-docs/blob/main/hibernation/kernel-6-11-workarounds/suspend-hibernate-bluetooth-workaround.md#workaround-for-suspendhibernate-black-screen-on-resume-kernel-611) (new tab), copy/paste the one liner, reboot. Now Bluetooth will stop for suspend and resume when you resume from s2idle suspend.~~ + + diff --git a/framework13/Ryzen-AI-300-Series.md b/framework13/Ryzen-AI-300-Series.md new file mode 100644 index 0000000..6fe7b39 --- /dev/null +++ b/framework13/Ryzen-AI-300-Series.md @@ -0,0 +1,92 @@ +# This is for Fedora on the Ryzen™ AI 300 Series Framework Laptop 13 ONLY. + +## This will: + +- Getting your laptop fully updated. +- Enable improved fractional scaling support Fedora's GNOME environment using Wayland. +- Enabling tap to click on the touchpad. + +  +  +  + + +### Step 1 Updating your software packages + +- Browse to the horizontal line in the upper left corner, click to open it. +- Type out the word terminal, click to open it. +- Copy the code below in the gray box, right click/paste it into the terminal window. +- Then press the enter key, user password, enter key, **reboot.** + + +``` +sudo dnf upgrade +``` +> **TIP:** You can use the little clipboard icon to the right of the code to copy to your clipboard. + + +**Reboot** + +  +  +  + +### Step 2 - If you want to enable fractional scaling on Wayland: + +- Type out the word Displays. +- Look for scale you want and select it, click Apply. + +  +  +  + +### Step 3 - If you want to enable "tap-to-click" on the touchpad: + +- Browse to the horizontal line in the upper left corner, click to open it. +- Type out the word mouse, look for Mouse and Touchpad, click to open it. +- Click the touchpad option at the top. +- Under "Clicking", select Tap to Click and enable it. + +  +  +  +### Bonus Step (for former Mac users) Reduce Font Scaling to Match Your Needs + +We received feedback that for users coming from OS X, installing GNOME Tweaks, browsing to Fonts, and reducing the font size from 1.00 to 0.80 may be preferred. + +- Goto Displays, set scaling to 200%. This will look too large, so let's fix the fonts. +- Install with: + +``` +sudo dnf install gnome-tweaks -y +``` + +- Open Tweaks by using the "Super" or Windows key, search tweaks, and enter. + +- At the top, select fonts. Now in that panel, scroll all the way down. Look for Size. Change from 1.00 to 0.80. Close Tweaks. + + Note: This is for the displays for the laptop only. This will look super odd on external displays and likely too large even still. + + + +  +  +  + +## Troubleshooting - Optional and *only if needed* - current AMD Ryzen AI 300 Series workarounds to common issues + +### Laggy or frozen system/processes: +(Customer submitted, not seeing this internally, but if you are, please file a bug so we can get this fixed vs this workaround please) + +- Browse to the horizontal line in the upper left corner, click to open it. +- Type out the word terminal, click to open it. +- Then press the ⏎ Enter key, user password, ⏎ Enter key. + +```bash +# Disable PSR + Panel Replay +sudo grubby --update-kernel=ALL --args="amdgpu.dcdebugmask=0x610" +``` +> **TIP:** If you've set other kernel parameters, like from the section above, include both inside `--args=""`. + + +**Reboot** diff --git a/framework13/Ubuntu-25-10.md b/framework13/Ubuntu-25-10.md new file mode 100644 index 0000000..7332fba --- /dev/null +++ b/framework13/Ubuntu-25-10.md @@ -0,0 +1,46 @@ +# This is for Ubuntu 25.10 on Framework Laptop 13 ONLY. + + +## This will: + +- Update your Ubuntu install's packages. +- Walk you getting tablet mode setup for Ubuntu 25.10 (ONLY) + +        + + +### Get everything updated + +- Browse to the upper left corner, click the horizontal line to open the menu. +- Type out the word terminal, click to open it. +- Click on the small icon shown in the image below to copy the code below in the gray box, right click/paste it into the terminal window. +- Then press the enter key, user password, enter key, **reboot.** + +``` +sudo apt update && sudo apt upgrade -y && sudo snap refresh +``` +> **TIP:** You can use the little clipboard icon to the right of the code to copy to your clipboard. + +

    Copy The Code Below Like This

    + +- **Then reboot** + +      + + +### Bonus Step (for former Mac users) Reduce Font Scaling to Match Your Needs + +We received feedback that for users coming from macOS, installing GNOME Tweaks, browsing to Fonts, and reducing the font size from 1.00 to 0.80 may be preferred. This will vary greatly how you have your fractional scaling setup in the Displays settings area. + +- Goto Displays, set scaling to 200%. This will look too large, so let's fix the fonts. +- Install with: + +``` +sudo apt update && sudo apt install gnome-tweaks -y +``` + +- Open Tweaks by using the "Super" or Windows key, search tweaks, and enter. + +- At the top, select fonts. Now in that panel, scroll all the way down. Look for Size. Change from 1.00 to 0.80. Close Tweaks. + + Note: This is for the displays for the laptop only. This will look super odd on external displays and likely too large even still. diff --git a/framework13/Ubuntu-26.04.md b/framework13/Ubuntu-26.04.md new file mode 100644 index 0000000..1c53e2f --- /dev/null +++ b/framework13/Ubuntu-26.04.md @@ -0,0 +1,46 @@ +# This is for Ubuntu 26.04 on Framework Laptop 13 ONLY. + + +## This will: + +- Update your Ubuntu install's packages. +- Walk you getting tablet mode setup for Ubuntu 26.04 (ONLY) + +        + + +### Get everything updated + +- Browse to the upper left corner, click the horizontal line to open the menu. +- Type out the word terminal, click to open it. +- Click on the small icon shown in the image below to copy the code below in the gray box, right click/paste it into the terminal window. +- Then press the enter key, user password, enter key, **reboot.** + +``` +sudo apt update && sudo apt upgrade -y && sudo snap refresh +``` +> **TIP:** You can use the little clipboard icon to the right of the code to copy to your clipboard. + +

    Copy The Code Below Like This

    + +- **Then reboot** + +      + + +### Bonus Step (for former Mac users) Reduce Font Scaling to Match Your Needs + +We received feedback that for users coming from macOS, installing GNOME Tweaks, browsing to Fonts, and reducing the font size from 1.00 to 0.80 may be preferred. This will vary greatly how you have your fractional scaling setup in the Displays settings area. + +- Goto Displays, set scaling to 200%. This will look too large, so let's fix the fonts. +- Install with: + +``` +sudo apt update && sudo apt install gnome-tweaks -y +``` + +- Open Tweaks by using the "Super" or Windows key, search tweaks, and enter. + +- At the top, select fonts. Now in that panel, scroll all the way down. Look for Size. Change from 1.00 to 0.80. Close Tweaks. + + Note: This is for the displays for the laptop only. This will look super odd on external displays and likely too large even still. diff --git a/framework13/Ubuntu24.04LTS-PRE-INSTALL-Setup-fw-13-PRO.md b/framework13/Ubuntu24.04LTS-PRE-INSTALL-Setup-fw-13-PRO.md new file mode 100644 index 0000000..776c2ed --- /dev/null +++ b/framework13/Ubuntu24.04LTS-PRE-INSTALL-Setup-fw-13-PRO.md @@ -0,0 +1,48 @@ +# This is for Intel® Core™ Ultra Series 3 Framework Laptop 13 Pro Ubuntu 24.04 ONLY. + +#### Officially supporting from Ubuntu 24.04 OEM install +Please use the **"Get everything updated"** section below if you are on standard Ubuntu 24.04 without the the "dot 1 release." + +## This will: + +- Update your Ubuntu install's packages. + +        + + +### Get everything updated + +- Browse to the upper left corner, click the horizontal line to open the menu. +- Type out the word terminal, click to open it. +- Click on the small icon shown in the image below to copy the code below in the gray box, right click/paste it into the terminal window. +- Then press the enter key, user password, enter key, **reboot.** + +``` +sudo apt update && sudo apt upgrade -y && sudo snap refresh +``` +> **TIP:** You can use the little clipboard icon to the right of the code to copy to your clipboard. + +

    Copy The Code Below Like This

    + + +      + + +### Bonus Step (for former Mac users) Reduce Font Scaling to Match Your Needs + +We received feedback that for users coming from OS X, installing GNOME Tweaks, browsing to Fonts, and reducing the font size from 1.00 to 0.80 may be preferred. + +- Goto Displays, set scaling to 200%. This will look too large, so let's fix the fonts. +- Install with: + +``` +sudo apt update && sudo apt install gnome-tweaks -y +``` + +- Open Tweaks by using the "Super" or Windows key, search tweaks, and enter. + +- At the top, select fonts. Now in that panel, scroll all the way down. Look for Size. Change from 1.00 to 0.80. Close Tweaks. + + Note: This is for the displays for the laptop only. This will look super odd on external displays and likely too large even still. + + diff --git a/framework13/Ubuntu26.04LTS-Setup-fw-13-PRO.md b/framework13/Ubuntu26.04LTS-Setup-fw-13-PRO.md new file mode 100644 index 0000000..75547f6 --- /dev/null +++ b/framework13/Ubuntu26.04LTS-Setup-fw-13-PRO.md @@ -0,0 +1,45 @@ +# This is for Ubuntu 26.04 on Framework Laptop 13 Pro ONLY. + + +## This will: + +- Update your Ubuntu install's packages. + +        + + +### Get everything updated + +- Browse to the upper left corner, click the horizontal line to open the menu. +- Type out the word terminal, click to open it. +- Click on the small icon shown in the image below to copy the code below in the gray box, right click/paste it into the terminal window. +- Then press the enter key, user password, enter key, **reboot.** + +``` +sudo apt update && sudo apt upgrade -y && sudo snap refresh +``` +> **TIP:** You can use the little clipboard icon to the right of the code to copy to your clipboard. + +

    Copy The Code Below Like This

    + +- **Then reboot** + +      + + +### Bonus Step (for former Mac users) Reduce Font Scaling to Match Your Needs + +We received feedback that for users coming from macOS, installing GNOME Tweaks, browsing to Fonts, and reducing the font size from 1.00 to 0.80 may be preferred. This will vary greatly how you have your fractional scaling setup in the Displays settings area. + +- Goto Displays, set scaling to 200%. This will look too large, so let's fix the fonts. +- Install with: + +``` +sudo apt update && sudo apt install gnome-tweaks -y +``` + +- Open Tweaks by using the "Super" or Windows key, search tweaks, and enter. + +- At the top, select fonts. Now in that panel, scroll all the way down. Look for Size. Change from 1.00 to 0.80. Close Tweaks. + + Note: This is for the displays for the laptop only. This will look super odd on external displays and likely too large even still. diff --git a/framework13/framework13-7040-series-nixos.md b/framework13/framework13-7040-series-nixos.md new file mode 100644 index 0000000..637262c --- /dev/null +++ b/framework13/framework13-7040-series-nixos.md @@ -0,0 +1,41 @@ +# Adding the NixOS-Hardware Module (Framework Laptop 13, AMD 7040 Series) + +This provides the hardware channel steps for the NixOS on the Framework Laptop 13 Guide. + +## Channel-based (default from graphical installer) + +```bash +sudo nix-channel --add https://github.com/NixOS/nixos-hardware/archive/master.tar.gz nixos-hardware +sudo nix-channel --update +``` + +```nix +# /etc/nixos/configuration.nix +imports = [ + ./hardware-configuration.nix + +]; +``` + +```bash +sudo nixos-rebuild switch +``` + +## Flake-Based + +```nix +# flake.nix inputs +inputs.nixos-hardware.url = "github:NixOS/nixos-hardware/master"; +``` + +```nix +# flake.nix modules list +modules = [ + ./configuration.nix + nixos-hardware.nixosModules.framework-13-7040-amd +]; +``` + +```bash +sudo nixos-rebuild switch --flake .# +``` diff --git a/framework13/framework13-ai-300-nixos.md b/framework13/framework13-ai-300-nixos.md new file mode 100644 index 0000000..ea99ada --- /dev/null +++ b/framework13/framework13-ai-300-nixos.md @@ -0,0 +1,41 @@ +# Adding the NixOS-Hardware Module (Framework Laptop 13, AMD AI 300 Series) + +This provides the hardware channel steps for the NixOS on the Framework Laptop 13 Guide. + +## Channel-based (default from graphical installer) + +```bash +sudo nix-channel --add https://github.com/NixOS/nixos-hardware/archive/master.tar.gz nixos-hardware +sudo nix-channel --update +``` + +```nix +# /etc/nixos/configuration.nix +imports = [ + ./hardware-configuration.nix + +]; +``` + +```bash +sudo nixos-rebuild switch +``` + +## Flake-Based + +```nix +# flake.nix inputs +inputs.nixos-hardware.url = "github:NixOS/nixos-hardware/master"; +``` + +```nix +# flake.nix modules list +modules = [ + ./configuration.nix + nixos-hardware.nixosModules.framework-amd-ai-300-series +]; +``` + +```bash +sudo nixos-rebuild switch --flake .# +``` diff --git a/framework13/framework13-intel-11th-gen-nixos.md b/framework13/framework13-intel-11th-gen-nixos.md new file mode 100644 index 0000000..2eb596a --- /dev/null +++ b/framework13/framework13-intel-11th-gen-nixos.md @@ -0,0 +1,41 @@ +# Adding the NixOS-Hardware Module (Framework Laptop 13, Intel 11th Gen) + +This provides the hardware channel steps for the NixOS on the Framework Laptop 13 Guide. + +## Channel-based (default from graphical installer) + +```bash +sudo nix-channel --add https://github.com/NixOS/nixos-hardware/archive/master.tar.gz nixos-hardware +sudo nix-channel --update +``` + +```nix +# /etc/nixos/configuration.nix +imports = [ + ./hardware-configuration.nix + +]; +``` + +```bash +sudo nixos-rebuild switch +``` + +## Flake-Based + +```nix +# flake.nix inputs +inputs.nixos-hardware.url = "github:NixOS/nixos-hardware/master"; +``` + +```nix +# flake.nix modules list +modules = [ + ./configuration.nix + nixos-hardware.nixosModules.framework-11th-gen-intel +]; +``` + +```bash +sudo nixos-rebuild switch --flake .# +``` diff --git a/framework13/framework13-intel-12th-gen-nixos.md b/framework13/framework13-intel-12th-gen-nixos.md new file mode 100644 index 0000000..e632483 --- /dev/null +++ b/framework13/framework13-intel-12th-gen-nixos.md @@ -0,0 +1,41 @@ +# Adding the NixOS-Hardware Module (Framework Laptop 13, Intel 12th Gen) + +This provides the hardware channel steps for the NixOS on the Framework Laptop 13 Guide. + +## Channel-based (default from graphical installer) + +```bash +sudo nix-channel --add https://github.com/NixOS/nixos-hardware/archive/master.tar.gz nixos-hardware +sudo nix-channel --update +``` + +```nix +# /etc/nixos/configuration.nix +imports = [ + ./hardware-configuration.nix + +]; +``` + +```bash +sudo nixos-rebuild switch +``` + +## Flake-Based + +```nix +# flake.nix inputs +inputs.nixos-hardware.url = "github:NixOS/nixos-hardware/master"; +``` + +```nix +# flake.nix modules list +modules = [ + ./configuration.nix + nixos-hardware.nixosModules.framework-12th-gen-intel +]; +``` + +```bash +sudo nixos-rebuild switch --flake .# +``` diff --git a/framework13/framework13-intel-13th-gen-nixos.md b/framework13/framework13-intel-13th-gen-nixos.md new file mode 100644 index 0000000..c34990c --- /dev/null +++ b/framework13/framework13-intel-13th-gen-nixos.md @@ -0,0 +1,41 @@ +# Adding the NixOS-Hardware Module (Framework Laptop 13, Intel 13th Gen) + +This provides the hardware channel steps for the NixOS on the Framework Laptop 13 Guide. + +## Channel-based (default from graphical installer) + +```bash +sudo nix-channel --add https://github.com/NixOS/nixos-hardware/archive/master.tar.gz nixos-hardware +sudo nix-channel --update +``` + +```nix +# /etc/nixos/configuration.nix +imports = [ + ./hardware-configuration.nix + +]; +``` + +```bash +sudo nixos-rebuild switch +``` + +## Flake-Based + +```nix +# flake.nix inputs +inputs.nixos-hardware.url = "github:NixOS/nixos-hardware/master"; +``` + +```nix +# flake.nix modules list +modules = [ + ./configuration.nix + nixos-hardware.nixosModules.framework-13th-gen-intel +]; +``` + +```bash +sudo nixos-rebuild switch --flake .# +``` diff --git a/framework13/framework13-intel-ultra-1-nixos.md b/framework13/framework13-intel-ultra-1-nixos.md new file mode 100644 index 0000000..2ad6794 --- /dev/null +++ b/framework13/framework13-intel-ultra-1-nixos.md @@ -0,0 +1,41 @@ +# Adding the NixOS-Hardware Module (Framework Laptop 13, Intel Core Ultra Series 1) + +This provides the hardware channel steps for the NixOS on the Framework Laptop 13 Guide. + +## Channel-based (default from graphical installer) + +```bash +sudo nix-channel --add https://github.com/NixOS/nixos-hardware/archive/master.tar.gz nixos-hardware +sudo nix-channel --update +``` + +```nix +# /etc/nixos/configuration.nix +imports = [ + ./hardware-configuration.nix + +]; +``` + +```bash +sudo nixos-rebuild switch +``` + +## Flake-Based + +```nix +# flake.nix inputs +inputs.nixos-hardware.url = "github:NixOS/nixos-hardware/master"; +``` + +```nix +# flake.nix modules list +modules = [ + ./configuration.nix + nixos-hardware.nixosModules.framework-intel-core-ultra-series1 +]; +``` + +```bash +sudo nixos-rebuild switch --flake .# +``` diff --git a/framework16/AI-300/Fedora-fw16-AI-300.md b/framework16/AI-300/Fedora-fw16-AI-300.md new file mode 100644 index 0000000..4cd295a --- /dev/null +++ b/framework16/AI-300/Fedora-fw16-AI-300.md @@ -0,0 +1,83 @@ +# Framework Laptop 16 (AMD Ryzen™ AI 300 Series) ONLY +### For Fedora Workstation + +## This will: + +- Get your laptop fully updated +- Enable improved fractional scaling support in Fedora's GNOME environment using Wayland +- Enable tap to click on the touchpad +- Prepare your system for gaming with hardware dGPU support as the next step (end of this article) + +  +  +  + +### Step 1: Update Your Software Packages + +- Browse to the horizontal line in the upper left corner, click to open it +- Type out the word "terminal", click to open it +- Copy the code below in the gray box, right click/paste it into the terminal window +- Then press the enter key, enter your user password, press enter key, **reboot** + +``` +sudo dnf upgrade -y +``` +> **TIP:** You can use the little clipboard icon to the right of the code to copy to your clipboard. + +**Reboot your system after this completes** + +  +  +  + +### Step 2: Enable Fractional Scaling on Wayland (Optional) + +- Browse to the horizontal line in the upper left corner, click to open it +- Type out the word "Displays" +- Look for "Scale", set it to your preference, click Apply + +  +  +  + +### Step 3: Enable "Tap-to-Click" on the Touchpad (Optional) + +- Browse to the horizontal line in the upper left corner, click to open it +- Type out the word "mouse", look for "Mouse and Touchpad", click to open it +- Click the touchpad option at the top +- Under "Clicking", select "Tap to Click" and enable it + +  +  +  + +### Bonus Step: Reduce Font Scaling (For Former Mac Users) + +For users coming from macOS, installing GNOME Tweaks and adjusting font scaling may provide a more familiar experience: + +- Go to Displays, set scaling to 200% (this will look too large initially) +- Install GNOME Tweaks: + +``` +sudo dnf install gnome-tweaks -y +``` + +- Open Tweaks by pressing the "Super" (Windows) key, search "tweaks", and press enter +- At the top, select "Fonts". Scroll down to find "Scaling Factor" +- Change from 1.00 to 0.80, then close Tweaks + +**Note:** This scaling adjustment is optimized for the laptop display only and may not look optimal on external monitors. + +  +  +  + +## Next Steps - NVIDIA drivers + +Continue with [installing NVIDIA drivers for Fedora](https://github.com/FrameworkComputer/linux-docs/blob/main/framework16/AI-300/nvidia-driver-install-Fedora.md#nvidia-dgpu-driver-installation-for-fedora). + +  +  + +---------------------------------------- +---------------------------------------- diff --git a/framework16/AI-300/Gaming-on-Steam-dGPU-Fedora.md b/framework16/AI-300/Gaming-on-Steam-dGPU-Fedora.md new file mode 100644 index 0000000..135e493 --- /dev/null +++ b/framework16/AI-300/Gaming-on-Steam-dGPU-Fedora.md @@ -0,0 +1,197 @@ +## Gaming on Steam with Fedora + +**Prerequisites:** You must first install the NVIDIA driver following the [NVIDIA dGPU Driver Installation guide](https://github.com/FrameworkComputer/linux-docs/blob/main/framework16/AI-300/nvidia-driver-install-Fedora.md#nvidia-dgpu-driver-installation-for-fedora). + +Once the NVIDIA driver is installed, your Framework Laptop 16's NVIDIA dGPU graphics automatically manages GPU usage. The integrated AMD graphics handle desktop tasks and light workloads for optimal battery life, while the discrete NVIDIA GPU automatically activates for demanding applications like games, 3D rendering, and compute tasks. This seamless switching ensures maximum performance when gaming while preserving battery life during regular use. + +Flatpak is the tested and recommended installation method for Steam. If you encounter issues using alternative installation methods, the support team will direct you back to this Flatpak approach shown below for troubleshooting. + + +**Install Steam via Flatpak** +``` +flatpak install flathub com.valvesoftware.Steam +``` + +**Enable game controller support** +``` +sudo dnf install steam-devices +``` + +### Storage Configuration + +**Single NVMe Drive** +If using only the main system drive, no additional configuration is needed. Steam will install games to your home directory by default. + +### Second NVMe Drive Configuration + +**Script-Based Configuration (Recommended)** +If you used the [Steam Drive Mounter script](https://github.com/FrameworkComputer/steam-drive-mounter/blob/main/README.md#steam-drive-mounter) for automated setup, the drive mounts with your username in the path. + +Next, replace `YourUserName` with your actual Fedora login name: + +``` +flatpak override --user --filesystem=/media/YourUserName/steamgames com.valvesoftware.Steam +``` +>If using mounter script linked above for secondary drive, skip Advanced Manual Configuration. + + +**Advanced Manual Configuration** +For manual setup of your second NVMe drive: + +1. Open Disks program, label the drive as `steamgames`, and format to Ext4. Close Disks. + +2. Open Terminal and create the mount point: +``` +cd /media && sudo mkdir steamgames +``` + +3. Set correct ownership and permissions: +``` +sudo chown $USER:$USER steamgames/ && sudo chmod 700 steamgames/ +``` + +4. Verify the setup: +``` +ls -ld steamgames/ +``` +You should see: `drwx------. 1 youruser youruser 0 Month day 00:00 steamgames/` + +5. Find your drive's UUID: +``` +sudo blkid | grep 'steamgames' | awk '{print $0}' +``` +Look for the UUID in the output: `UUID="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"` + +6. Backup fstab and edit it: +``` +sudo cp /etc/fstab /etc/fstab.bak && sudo nano /etc/fstab +``` + +7. Add this line to the bottom (using YOUR UUID): +``` +UUID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx /media/steamgames ext4 rw,users,exec,auto 0 0 +``` + +8. Save with Ctrl+X, then Y, and reboot. + +### Configure Flatpak permissions and Mounter script or manual method completion +``` +flatpak override --user --filesystem=/media/steamgames com.valvesoftware.Steam +``` + +Add the drive in Steam: + - Open Steam → Settings → Storage + - Click "Local Drive" pulldown menu + - Click "Add Drive" and navigate to /media/steamgames or /media/YourUser/steamgames + - Make your selection. + +### NVIDIA Driver Maintenance + +After any NVIDIA driver update, update the Flatpak NVIDIA runtime to maintain compatibility: + +``` +flatpak update org.freedesktop.Platform.GL.nvidia +``` + +----------- + +## Troubleshooting + +### Steam Won't Launch or Crashes +**Check NVIDIA driver status:** +``` +nvidia-smi +``` +If this fails, reinstall the NVIDIA driver following the prerequisites guide. + +**Update Flatpak NVIDIA runtime:** +``` +flatpak update org.freedesktop.Platform.GL.nvidia +``` + +**Reset Steam's Flatpak data:** +``` +flatpak uninstall --delete-data com.valvesoftware.Steam +flatpak install flathub com.valvesoftware.Steam +``` + +### Games Using Integrated Graphics Instead of NVIDIA dGPU +**Verify NVIDIA is detected:** +``` +glxinfo | grep "OpenGL renderer" +``` +Should show NVIDIA GPU information. + +### Steam Can't See Second NVMe Drive +**Verify mount is working:** +Remember, path will be determined if you did the [secondary NVME setup maunally or not](#second-nvme-drive-configuration). +``` +df -h | grep steamgames +ls -la /media/steamgames +``` + +**Check Flatpak permissions:** +``` +flatpak override --user --show com.valvesoftware.Steam +``` +Should list your steamgames filesystem path. + +**Restart Steam completely:** +``` +flatpak kill com.valvesoftware.Steam +``` +Then relaunch Steam. + +### Poor Game Performance +**Check GPU usage during gaming:** +(Might need to [install it first](https://github.com/FrameworkComputer/linux-docs/blob/main/framework16/AI-300/graphics-usage-detection.md#discrete-graphics-usage-detection)) +``` +nvtop +``` +GPU usage should be high (70%+) when gaming. + +**Run using the best power mode:** +In GNOME, upper right, pull down menu of your desktop where you would power off your system. Look for "Power Mode", enable it for performance. + +### Controller Not Working +**Check if steam-devices is installed:** +``` +rpm -qa | grep steam-devices +``` + +**Test controller detection:** +You will need to install evtest first: _sudo dnf install evtest_ + +``` +evtest +``` +Your controller should appear in the list. + +**Restart Steam after connecting controller:** +Some controllers require Steam to be restarted after connection. + +### Game Won't Start or Black Screen +- **Check Proton compatibility:** +Try different Proton versions in Steam → Settings → Compatibility. + +- **Check Wayland compatibility:** +Some games work better on X11. Log out and select "GNOME on Xorg" at login. + +### Audio Issues in Games +**Check PipeWire status:** +``` +systemctl --user status pipewire +``` + +**Restart audio services:** +``` +systemctl --user restart pipewire pipewire-pulse +``` + +### Need More Help? +1. Check the [Steam Flatpak FAQ](https://github.com/flathub/com.valvesoftware.Steam/wiki/) +2. Generate system info: Steam → Help → Steam Runtime Diagnostics (provide to Framework support) +3. Check game-specific issues on [ProtonDB](https://www.protondb.com/) +4. Updated your drivers recently? Make sure flatpak is also up to date _flatpak update org.freedesktop.Platform.GL.nvidia_ +5. Visit Framework Community forums with your system info and specific error messages +6. Check for Steam updates: _flatpak update com.valvesoftware.Steam_ diff --git a/framework16/AI-300/Gaming-on-Steam-dGPU-Ubuntu.md b/framework16/AI-300/Gaming-on-Steam-dGPU-Ubuntu.md new file mode 100644 index 0000000..9c2d83c --- /dev/null +++ b/framework16/AI-300/Gaming-on-Steam-dGPU-Ubuntu.md @@ -0,0 +1,164 @@ +## Gaming on Steam on Ubuntu + +**Prerequisites:** You must first have NVIDIA drivers installed. If you selected **"Install third-party software for graphics and Wi-Fi hardware"** during Ubuntu installation, this is already done. Otherwise, install drivers via Settings → Additional Drivers. + +Once the NVIDIA driver is installed, your Framework Laptop 16's NVIDIA dGPU graphics automatically manages GPU usage. The integrated AMD graphics handle desktop tasks and light workloads for optimal battery life, while the discrete NVIDIA GPU automatically activates for demanding applications like games, 3D rendering, and compute tasks. This seamless switching ensures maximum performance when gaming while preserving battery life during regular use. + +**Install Steam via Official .deb Package** + +1. Download the Steam .deb package from https://store.steampowered.com/about/ +2. Click "Install Steam" → "Download Steam for Linux" +3. Open your Downloads folder, double-click the .deb file +4. Ubuntu Software will open, click "Install" +5. Enter your password when prompted + + +### Storage Configuration + +**Single NVMe Drive** +If using only the main system drive, no additional configuration is needed. Steam will install games to your home directory by default. + +### Second NVMe Drive Configuration + +**Script-Based Configuration (Recommended)** +If you used the [Steam Drive Mounter script](https://github.com/FrameworkComputer/steam-drive-mounter/blob/main/README.md#steam-drive-mounter) for automated setup, the drive mounts with your username in the path. + +>If using mounter script linked above for secondary drive, skip Advanced Manual Configuration. + +**Advanced Manual Configuration** +For manual setup of your second NVMe drive: + +1. Open Disks application, label the drive as `steamgames`, and format to Ext4. Close Disks. + +2. Open Terminal and create the mount point: + +`cd /media && sudo mkdir steamgames` + +3. Set correct ownership and permissions: + +```sudo chown $USER:$USER steamgames/ && sudo chmod 700 steamgames/``` + +4. Verify the setup: + +```ls -ld steamgames/``` + +You should see: `drwx------. 1 youruser youruser 0 Month day 00:00 steamgames/` + +5. Find your drive's UUID: + +```sudo blkid | grep 'steamgames' | awk '{print $0}'``` + +Look for the UUID in the output: `UUID="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"` + +6. Backup fstab and edit it: + +```sudo cp /etc/fstab /etc/fstab.bak && sudo nano /etc/fstab``` + +7. Add this line to the bottom (using YOUR UUID): + +```UUID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx /media/steamgames ext4 rw,users,exec,auto 0 0``` + +8. Save with Ctrl+X, then Y, and reboot. + +### Mounter script or manual method completion + +After reboot, add the drive in Steam: + - Open Steam → Settings → Storage + - Click "Local Drive" pulldown menu + - Click "Add Drive" and navigate to /media/steamgames or /media/YourUser/steamgames + - Make your selection. + +----------- + +## Troubleshooting + +### Steam Won't Launch or Crashes +**Check NVIDIA driver status:** + +`nvidia-smi` + +If this fails, go to Settings → Additional Drivers and install the **recommended** NVIDIA driver. It will be _labled as recommended_. + +**Reinstall Steam:** + +`sudo apt remove steam` + +1. Open your Downloads folder, double-click the .deb file +2. Ubuntu Software will open, click "Install" +3. Enter your password when prompted + + +### Games Using Integrated Graphics Instead of NVIDIA dGPU +**Verify NVIDIA is detected:** + +`glxinfo | grep "OpenGL renderer"` + +Should show NVIDIA GPU information. + + +### Steam Can't See Second NVMe Drive +**Verify mount is working:** + +`df -h | grep steamgames` + +`ls -la /media/steamgames` + +**Check permissions:** + +`sudo chown -R $USER:$USER /media/steamgames` + +**Restart Steam completely:** +Close Steam, then in terminal: + +`killall steam` + +`steam` + +### Poor Game Performance +**Check GPU usage during gaming:** + +`nvtop` + +GPU usage should be high (70%+) when gaming. + +**Install gamemode for optimized performance:** + +Run using the best power mode: In GNOME, upper right, pull down menu of your desktop where you would power off your system. Look for "Power Mode", enable it for performance. + +### Controller Not Working +**Install controller support:** + +`sudo apt install steam-devices` + +**Test controller detection:** + +`sudo apt install evtest` + +`evtest` + +Your controller should appear in the list. + +**Restart Steam after connecting controller:** +Some controllers require Steam to be restarted after connection. + +### Game Won't Start or Black Screen +- **Check Proton compatibility:** Try different Proton versions in Steam → Settings → Compatibility. + +- **Check Wayland compatibility:** Some games work better on X11. Log out and select "GNOME on Xorg" at login. + + +### Audio Issues in Games +**Check PulseAudio/PipeWire status:** + +`systemctl --user status pipewire` + +**Restart audio services:** + +`systemctl --user restart pipewire pipewire-pulse` + +### Need More Help? +1. Generate system info: Steam → Help → Steam Runtime Diagnostics (provide to Framework support) +3. Check game-specific issues on [ProtonDB](https://www.protondb.com/) +5. Visit Framework Community forums with your system info and specific error messages +6. Check for Steam updates: Steam menu → Check for steam client updates. + diff --git a/framework16/AI-300/Ubuntu-25.10+-fw16-AI-300.md b/framework16/AI-300/Ubuntu-25.10+-fw16-AI-300.md new file mode 100644 index 0000000..539aa5b --- /dev/null +++ b/framework16/AI-300/Ubuntu-25.10+-fw16-AI-300.md @@ -0,0 +1,180 @@ +# Framework Laptop 16 (AMD Ryzen™ AI 300 Series) ONLY +### For Ubuntu 25.10 and greater recommended +(Ubuntu 25.10 is exected in Oct of 2025) + +## This will: + +- Get your laptop fully updated +- Install NVIDIA driver +- Install GPU monitoring tools + +  +  +  + +### Important: During Ubuntu Installation + +When installing Ubuntu, if you followed our guide, you remembered to enable **"Install third-party software for graphics and Wi-Fi hardware"** as this will automatically handle the NVIDIA driver installation for you. + +  +  +  + +### Step 1: Update System and Install CUDA Support + +- Open Terminal +- Copy the code below in the gray box, right click/paste it into the terminal window +- Then press the enter key, enter your user password, press enter key, **reboot** + +``` +sudo apt update && sudo apt upgrade -y && sudo apt install nvtop +``` + +>**Note:** ollama, text-generation-webui, vllm, oobabooga images that already bundle CUDA), so nothing needed here once your driver is installed. + +**Reboot your system after this completes** + +  +  +  + +### Step 2: Enable Fractional Scaling on Wayland (Optional) + +- Click on the top right corner, select Settings +- Navigate to "Displays" +- Look for "Scale", set it to your preference (125%, 150%, 175%, or 200%), click Apply + +  +  +  + +### Step 3: Enable "Tap-to-Click" on the Touchpad (Optional) + +- Click on the top right corner, select Settings +- Navigate to "Mouse & Touchpad" +- Under "Touchpad" section, toggle on "Tap to Click" + +### Bonus Step: Reduce Font Scaling (For Former Mac Users) + +For users coming from macOS, installing GNOME Tweaks and adjusting font scaling may provide a more familiar experience: + +- Go to Displays, set scaling to 200% (this will look too large initially) +- Install GNOME Tweaks: + +``` +sudo apt update && sudo apt install gnome-tweaks -y +``` + +- Open Tweaks by pressing the "Super" (Windows) key, search "tweaks", and press enter +- At the top, select "Fonts". Scroll down to find "Scaling Factor" +- Change from 1.00 to 0.80, then close Tweaks + +**Note:** This scaling adjustment is optimized for the laptop display only and may not look optimal on external monitors. + +  + +------------------------------------ + +## Verify NVIDIA driver installation + +`modinfo -F version nvidia` + +This will tell you your installed NVIDIA driver version. + +**Your NVIDIA driver is installed and ready for dGPU enabled Steam gaming, compute tasks, NVENC GPU rendering for video editors, etc.** + +### How to determine if your dGPU is active + +Run nvtop from the terminal. Your dGPU will be clearly labled at the top of the terminal output. You will only see activitity from nvtop for the dDPU when Steam gaming or when a workload is calling upon the dGPU to run. + +`nvtop` + + + +### Important +- We recommend using the installation method step listed under "Install third-party software for graphics and Wi-Fi hardware." Building the driver yourself or deviating from this at all whill yield varied results that are not something we tested agaist for this guide. +- When seeking support from the support team at Framework, we will be verifying you followed these directions. This is the driver handling method the support team has vetted as working and reliable. + +## Next Steps - Steam Gaming with NVIDIA + +Continue with [Gaming on Steam](https://github.com/FrameworkComputer/linux-docs/blob/main/framework16/AI-300/Gaming-on-Steam-dGPU-Ubuntu.md#gaming-on-steam-on-ubuntu) + +----------------- +  +  + +------------------------- + +## NVIDIA driver Troubleshooting + +> **Q: What if the driver failed to install?** +> +> A: If you didn't install drivers during OS install or if that failed for some reason, [check this upstream documentation](https://documentation.ubuntu.com/server/how-to/graphics/install-nvidia-drivers/) for next steps to correct this. +> +>**Q: dGPU is not doing anything or does not seem to be working?** +> A: Did you run nvidia-smi to verify you're detecting the nvidia driver? You understand that not all applications use the dGPU, even when pressed into service to do so. Browsers and other applications will not use the dGPU as there is no reason to do so. +> +> **Q: The dGPU worked previously, ran updates, now it is not working anymore, what happened?** +> A: If you installed the NVIDIA driver through Additional Drivers as recommended, check if a kernel update has occurred. You may need to reboot or reinstall the driver through Additional Drivers. If issues persist, open a support ticket as a regression may have been introduced. +> +> **Q: The NVIDIA module is installed as outlined from the dGPU installation guide, but there is question as to whether it's actually being detected at all?** +> A: From a terminal, run nvidia-smi to verify the driver is loaded. Also, you can make sure the dGPU is physically seen as present with this terminal command: +> +> `sudo lshw -C display` +> +> (The NVIDIA dGPU should appear as "product: GeForce RTX 5070 Series.") +> +> **Q: I'm using Secure Boot and the driver installation failed or isn't working properly. What should I do?** +> +> A: Ubuntu should automatically detect Secure Boot and use pre-signed kernel modules during installation. However, if this fails, follow these fallback steps: +> +> **Step 1: Completely remove all existing NVIDIA packages** +> ``` +> # First, identify installed NVIDIA packages and their driver branch numbers +> apt-mark showmanual | grep nvidia +> +> # Remove all NVIDIA packages (replace XXX with your driver branch number, e.g., 550) +> sudo apt --purge remove '*nvidia*XXX*' +> +> # Clean up any remaining dependencies +> sudo apt autoremove +> ``` +> +> **Step 2: Manually install the driver with Secure Boot support** +> +> For systems with Secure Boot enabled, use the pre-compiled signed kernel modules: +> ``` +> # First, check available drivers +> sudo ubuntu-drivers list +> +> # Install pre-compiled signed modules for your kernel +> # Replace XXX with your driver branch (e.g., 550) +> # Replace 'generic' with your kernel flavour if different +> sudo apt install linux-modules-nvidia-XXX-generic +> +> # Verify modules were installed for your current kernel +> sudo apt-cache policy linux-modules-nvidia-XXX-$(uname -r) +> +> # If not installed for current kernel, install specifically +> sudo apt install linux-modules-nvidia-XXX-$(uname -r) +> +> # Finally, install the driver metapackage +> sudo apt install nvidia-driver-XXX +> +> # Reboot your system +> sudo reboot +> ``` +> +> After reboot, verify the installation with `nvidia-smi` and `modinfo -F version nvidia`. +> +> **Note:** The ubuntu-drivers tool is recommended for Secure Boot systems as it automatically handles signed drivers. Only use the manual method if the automatic installation fails. +> +> **Q: Still having issues and need help?** +> A: Please open [a support ticket](https://framework.kustomer.help/contact/support-request-ryon9uAuq). +> +> **Q: But I need a simple, reliable, tested method for pushing other applications to the dGPU?** +> +> A: It's [ready, GPU switcher for Framework Laptop 16 with NVIDIA dGPU](https://github.com/FrameworkComputer/GPUMode?tab=readme-ov-file#gpumode). See below. + +[![GPUMode - System tray application for manual GPU mode switching on Ubuntu 25.10 Framework Laptop 16 with AMD integrated and NVIDIA discrete graphics.](https://raw.githubusercontent.com/FrameworkComputer/linux-docs/refs/heads/main/framework16/AI-300/images/hybrid.png)](https://guides.frame.work/Guide/Ubuntu+25.10+Installation+on+the+Framework+Laptop+16/585?lang=en#s3133) + diff --git a/framework16/AI-300/framework16-ai-300-nixos.md b/framework16/AI-300/framework16-ai-300-nixos.md new file mode 100644 index 0000000..6ecd14c --- /dev/null +++ b/framework16/AI-300/framework16-ai-300-nixos.md @@ -0,0 +1,56 @@ +# Adding the NixOS-Hardware Module (Framework Laptop 16, AMD Ryzen AI 300 Series) + +This provides the hardware channel steps for the NixOS on the Framework Laptop 16 Guide. + +It is recommended to use `power-profiles-daemon` over `tlp` for the AMD Framework 16. + +## What this module does + +- Imports the common AMD hardware configuration +- Pins to `linuxPackages_latest` if your kernel is older than 6.15, since 6.14+ is the minimum recommended kernel for this generation +- Enables `services.fwupd.enable`, so firmware updates are handled automatically +- Applies three `amdgpu` kernel parameters (`dcdebugmask=0x410`, `sg_display=0`, `abmlevel=0`) that fix known graphics issues on this generation +- Enables `services.fprintd.enable`, for fingerprint reader support +- Enables `hardware.keyboard.qmk.enable` and adds a libinput quirks override, for the Framework 16 keyboard module +- Enables `hardware.sensor.iio.enable`, needed for desktop environments to detect and manage display brightness +- Adds a udev rule fixing USB autosuspend on the Ethernet expansion card + +If you have the NVIDIA dGPU expansion module, use the separate NVIDIA submodule instead of this one (it already includes everything above). + +## Channel-based (default from graphical installer) + +```bash +sudo nix-channel --add https://github.com/NixOS/nixos-hardware/archive/master.tar.gz nixos-hardware +sudo nix-channel --update +``` + +```nix +# /etc/nixos/configuration.nix +imports = [ + ./hardware-configuration.nix + +]; +``` + +```bash +sudo nixos-rebuild switch +``` + +## Flake-Based + +```nix +# flake.nix inputs +inputs.nixos-hardware.url = "github:NixOS/nixos-hardware/master"; +``` + +```nix +# flake.nix modules list +modules = [ + ./configuration.nix + nixos-hardware.nixosModules.framework-16-amd-ai-300-series +]; +``` + +```bash +sudo nixos-rebuild switch --flake .# +``` diff --git a/framework16/AI-300/framework16-ai-300-w-NVIDIA-dGPU-nixos.md b/framework16/AI-300/framework16-ai-300-w-NVIDIA-dGPU-nixos.md new file mode 100644 index 0000000..4030229 --- /dev/null +++ b/framework16/AI-300/framework16-ai-300-w-NVIDIA-dGPU-nixos.md @@ -0,0 +1,62 @@ +# Adding NVIDIA dGPU Support (Framework Laptop 16, AMD Ryzen AI 300 Series) + +If your Framework Laptop 16 has the NVIDIA dGPU expansion module, use the nixos-hardware NVIDIA submodule instead of setting driver options manually. It enables hybrid graphics with PRIME offload: the AMD iGPU runs by default for better battery life, and the NVIDIA dGPU is used on demand via the `nvidia-offload` command. This submodule already includes the base AMD Ryzen AI 300 Series module, so you only need to add this one, not both. + +**Important:** the PCI bus IDs for your GPUs vary depending on installed expansion cards and NVMe drives. You must override them for your specific system. + +```bash +nix-shell -p pciutils --run 'lspci | grep -E "VGA|3D|Display"' +``` + +## Channel-based (default from graphical installer) + +```bash +sudo nix-channel --add https://github.com/NixOS/nixos-hardware/archive/master.tar.gz nixos-hardware +sudo nix-channel --update +``` + +```nix +# /etc/nixos/configuration.nix +imports = [ + ./hardware-configuration.nix + +]; + +hardware.nvidia.prime = { + # Replace these with your own system's bus IDs from the lspci command above + amdgpuBusId = "PCI:XXX:YY:Z"; + nvidiaBusId = "PCI:AAA:BB:C"; +}; +``` + +```bash +sudo nixos-rebuild switch +``` + +## Flake-Based + +```nix +# flake.nix inputs +inputs.nixos-hardware.url = "github:NixOS/nixos-hardware/master"; +``` + +```nix +# flake.nix modules list +modules = [ + ./configuration.nix + nixos-hardware.nixosModules.framework-16-amd-ai-300-series-nvidia +]; +``` + +```nix +# In your system configuration +hardware.nvidia.prime = { + # Replace these with your own system's bus IDs from the lspci command above + amdgpuBusId = "PCI:XXX:YY:Z"; + nvidiaBusId = "PCI:AAA:BB:C"; +}; +``` + +```bash +sudo nixos-rebuild switch --flake .# +``` diff --git a/framework16/AI-300/graphics-usage-detection.md b/framework16/AI-300/graphics-usage-detection.md new file mode 100644 index 0000000..32cb531 --- /dev/null +++ b/framework16/AI-300/graphics-usage-detection.md @@ -0,0 +1,44 @@ +# Discrete graphics usage detection + +(Updated) Now recommending using nvtop for both AMD and [NVIDIA dGPUs](https://github.com/FrameworkComputer/linux-docs/blob/main/framework16/AI-300/nvidia-driver-install-Fedora.md#nvidia-dgpu-driver-installation-for-fedora)) + +# Installing nvtop on Ubuntu and Fedora + +`nvtop` (NVIDIA TOP) is a real-time GPU monitoring tool similar to `htop`. +It shows GPU utilization, temperature, memory usage, and active processes. + +--- + +## Ubuntu + +### Install for Ubuntu +```sudo apt update && sudo apt install nvtop``` + +### Run +`nvtop` + +--- + +## Fedora + +### Install for Fedora +```sudo dnf install nvtop``` + +### Run +`nvtop` + +--- + +## Usage + +- Launch with `nvtop` in the terminal +- Navigate with the arrow keys +- Press `q` to quit + + + +------------------------------------------------------- + +  +  +   diff --git a/framework16/AI-300/images/NVIDIA-GPU-Manager-Ubuntu.png b/framework16/AI-300/images/NVIDIA-GPU-Manager-Ubuntu.png new file mode 100644 index 0000000..fd23e31 Binary files /dev/null and b/framework16/AI-300/images/NVIDIA-GPU-Manager-Ubuntu.png differ diff --git a/framework16/AI-300/images/NVIDIA-GPU-Manager.png b/framework16/AI-300/images/NVIDIA-GPU-Manager.png new file mode 100644 index 0000000..d2d1d42 Binary files /dev/null and b/framework16/AI-300/images/NVIDIA-GPU-Manager.png differ diff --git a/framework16/AI-300/images/any-key.png b/framework16/AI-300/images/any-key.png new file mode 100644 index 0000000..d35a5ed Binary files /dev/null and b/framework16/AI-300/images/any-key.png differ diff --git a/framework16/AI-300/images/continue.png b/framework16/AI-300/images/continue.png new file mode 100644 index 0000000..86ab5fe Binary files /dev/null and b/framework16/AI-300/images/continue.png differ diff --git a/framework16/AI-300/images/enable-graphics.png b/framework16/AI-300/images/enable-graphics.png new file mode 100644 index 0000000..8467de1 Binary files /dev/null and b/framework16/AI-300/images/enable-graphics.png differ diff --git a/framework16/AI-300/images/enable-graphics2.png b/framework16/AI-300/images/enable-graphics2.png new file mode 100644 index 0000000..c0e5e43 Binary files /dev/null and b/framework16/AI-300/images/enable-graphics2.png differ diff --git a/framework16/AI-300/images/enable-graphics3.png b/framework16/AI-300/images/enable-graphics3.png new file mode 100644 index 0000000..b37ff03 Binary files /dev/null and b/framework16/AI-300/images/enable-graphics3.png differ diff --git a/framework16/AI-300/images/enable-graphics4.png b/framework16/AI-300/images/enable-graphics4.png new file mode 100644 index 0000000..d9171ce Binary files /dev/null and b/framework16/AI-300/images/enable-graphics4.png differ diff --git a/framework16/AI-300/images/enroll.png b/framework16/AI-300/images/enroll.png new file mode 100644 index 0000000..b4353fd Binary files /dev/null and b/framework16/AI-300/images/enroll.png differ diff --git a/framework16/AI-300/images/hybrid.png b/framework16/AI-300/images/hybrid.png new file mode 100644 index 0000000..8061782 Binary files /dev/null and b/framework16/AI-300/images/hybrid.png differ diff --git a/framework16/AI-300/images/password-enter.png b/framework16/AI-300/images/password-enter.png new file mode 100644 index 0000000..161f501 Binary files /dev/null and b/framework16/AI-300/images/password-enter.png differ diff --git a/framework16/AI-300/images/reboot-now.png b/framework16/AI-300/images/reboot-now.png new file mode 100644 index 0000000..7916631 Binary files /dev/null and b/framework16/AI-300/images/reboot-now.png differ diff --git a/framework16/AI-300/images/yes-enroll.png b/framework16/AI-300/images/yes-enroll.png new file mode 100644 index 0000000..86ca83b Binary files /dev/null and b/framework16/AI-300/images/yes-enroll.png differ diff --git a/framework16/AI-300/nvidia-driver-install-Fedora.md b/framework16/AI-300/nvidia-driver-install-Fedora.md new file mode 100644 index 0000000..f2355b9 --- /dev/null +++ b/framework16/AI-300/nvidia-driver-install-Fedora.md @@ -0,0 +1,162 @@ +# NVIDIA dGPU Driver Installation for Fedora + +Your Framework Laptop 16 with Ryzen AI 300 series CPU includes an option for an optioonal discrete NVIDIA GPU module. On a fresh Fedora installation, the open-source nouveau driver is automatically detected and ready for basic display functionality. However, for gaming performance, hardware video encoding/decoding (NVENC), CUDA compute workloads, and optimal dGPU utilization, you'll need the proprietary NVIDIA driver from RPM Fusion. + +## Installing NVIDIA Drivers for Gaming and Intensive Tasks + +**Update system and install NVIDIA proprietary drivers with hardware acceleration** + +Open up a terminal window, paste in the follow line below followed by the enter key and your Fedora login password when asked. + +``` +sudo dnf update -y && sudo dnf install akmod-nvidia xorg-x11-drv-nvidia-cuda-libs nvtop +``` + +>**Note:** While CUDA (xorg-x11-drv-nvidia-cuda-libs) is optional, if you are entertaining using local LLMs (AI tools), use the default command which includes xorg-x11-drv-nvidia-cuda-libs. This allows LLMs to work correctly on Fedora. + +### IMPORTANT: Secure Boot MOK Enrollment Process + +**If your system has Secure Boot enabled (most modern systems do), you MUST complete the following steps:** + +During the akmod-nvidia installation: +- Modules are compiled and signed automatically +- You'll be prompted to create a password - **REMEMBER THIS PASSWORD** +- The signing key is staged for enrollment on next boot + +![Enable graphics](https://raw.githubusercontent.com/FrameworkComputer/linux-docs/refs/heads/main/framework16/AI-300/images/enable-graphics.png) +![Enable graphics step 2](https://raw.githubusercontent.com/FrameworkComputer/linux-docs/refs/heads/main/framework16/AI-300/images/enable-graphics2.png) +![Enable graphics step 3](https://raw.githubusercontent.com/FrameworkComputer/linux-docs/refs/heads/main/framework16/AI-300/images/enable-graphics3.png) +![Enable graphics step 4](https://raw.githubusercontent.com/FrameworkComputer/linux-docs/refs/heads/main/framework16/AI-300/images/enable-graphics4.png) + + +**After installation completes, reboot the laptop** + +### Enrolling Self-signing Key after Reboot + +**This screen appears only ONCE before normal boot - DO NOT SKIP IT:** + +1. Press any key to continue when you see "Press any key to perform MOK management" + +![Press any key to continue](https://raw.githubusercontent.com/FrameworkComputer/linux-docs/refs/heads/main/framework16/AI-300/images/any-key.png) + + +2. Select **Enroll MOK** + +![Enroll MOK](https://raw.githubusercontent.com/FrameworkComputer/linux-docs/refs/heads/main/framework16/AI-300/images/enroll.png) + + +4. Select **Continue** to proceed to the enrollment + +![Continue to enrollment](https://raw.githubusercontent.com/FrameworkComputer/linux-docs/refs/heads/main/framework16/AI-300/images/continue.png) + + +5. Select **Yes** to enroll the key + +![Yes to enroll](https://raw.githubusercontent.com/FrameworkComputer/linux-docs/refs/heads/main/framework16/AI-300/images/yes-enroll.png) + + +6. Type the password you created during installation + +![Enter password](https://raw.githubusercontent.com/FrameworkComputer/linux-docs/refs/heads/main/framework16/AI-300/images/password-enter.png) + + +7. Select **Reboot** to reboot into the OS with the NVIDIA drivers enabled + +![Reboot now](https://raw.githubusercontent.com/FrameworkComputer/linux-docs/refs/heads/main/framework16/AI-300/images/reboot-now.png) + + +**Once booted back into your laptop, verify installation with:** + +``` +modinfo -F version nvidia +``` + +This will tell you your installed NVIDIA driver version. + +**Your NVIDIA driver is installed and ready for dGPU enabled Steam gaming, compute tasks, NVENC GPU rendering for video editors, etc.** + +### How to determine if your dGPU is active + +[Install and run nvtop](https://github.com/FrameworkComputer/linux-docs/blob/main/framework16/AI-300/graphics-usage-detection.md#discrete-graphics-usage-detection). Your dGPU will be clearly labled at the top of the terminal output. You will only see activitity from nvtop for the dDPU when Steam gaming or when a workload is calling upon the dGPU to run. + +### Important +- We recommend using the dnf installation method step listed above under "Installing NVIDIA Drivers for Gaming and Intensive Tasks". Building the driver yourself or deviating from this at all whill yield varied results that are not something we tested agaist for this guide. +- When seeking support from the support team at Framework, we will be verifying you followed these directions. This is the driver handling method the support team has vetted as working and reliable. + +## Next Steps - Steam Gaming with NVIDIA + +Continue with [Gaming on Steam](https://github.com/FrameworkComputer/linux-docs/blob/main/framework16/AI-300/Gaming-on-Steam-dGPU-Fedora.md#gaming-on-steam) + +----------------- +----------------- + +## Troubleshooting + +> **Q: dGPU is not doing anything or does not seem to be working?** +> A: Did you run the modinfo command above to verify you're detecting the nvidia driver? You understand that not all applications use the dGPU, even when pressed into service to do so. Browsers and other applications will not use the dGPU as there is no reason to do so. +> +> **Q: The dGPU worked previously, ran updates, now it is not working anymore, what happened?** +> A: Assuming you used the NVIDIA driver command provided above, did not try enabling rawhide or building it yourself with other means outside of the instructions provided, we would want you to open a support ticket in case a regression has been introduced in akmod-nvidia. +> +> **Q: The NVIDIA module is installed as outlined from the dGPU installation guide, but there is question as to whether it's actually being detected at all?** +> A: From a terminal, run the modinfo command listed above under "verify installation with" - also, you can make sure the dGPU is physically seen as present with this terminal command: +> ``` +> sudo dnf install lshw -y && sudo lshw -C display +> ``` +> (The NVIDIA dGPU should appear as "product: GeForce RTX 5070 Series.") +> +> **Q: I missed/skipped the blue MOK enrollment screen and my NVIDIA driver isn't working. What happened and how do I fix it?** +> +> A: If you missed the MOK screen: +> - Your system booted normally using the Nouveau driver (fallback open-source driver) +> - You have a working desktop with basic graphics functionality +> - The NVIDIA driver is installed but WON'T load due to Secure Boot blocking unsigned modules +> +> **To fix a missed MOK enrollment:** +> 1. Open a terminal in your working session (currently using Nouveau) +> 2. Re-stage the signing key for enrollment: +> ``` +> sudo mokutil --import /etc/pki/akmods/certs/public_key.der +> ``` +> 3. Create a NEW password when prompted (remember this new password!) +> 4. Reboot your system: +> ``` +> sudo reboot +> ``` +> 5. The blue MOK screen will appear again - **DON'T MISS IT THIS TIME** +> 6. Follow the enrollment steps with your NEW password: +> - Press any key to continue +> - Select **Enroll MOK** +> - Select **Continue** to proceed to the enrollment +> - Select **Yes** to enroll the key +> - Type the password you created +> - Select **Reboot** to reboot into the OS +> +> **After successful enrollment:** +> - System boots with NVIDIA driver working +> - Full GPU acceleration enabled +> - Future driver updates automatically signed with enrolled key +> - No more MOK prompts needed +> +> **Q: The NVIDIA driver still isn't working after MOK enrollment (Nouveau fallback on reboot)?** +> +> A: If the system falls back to Nouveau even after successful MOK enrollment, try these steps: +> ``` +> sudo dnf install kernel-devel-$(uname -r) kernel-headers-$(uname -r) +> sudo akmods --kernels $(uname -r) --rebuild +> sudo depmod -a +> sudo dracut --force --kver $(uname -r) +> echo 'nvidia-drm.modeset=1' | sudo tee -a /etc/kernel/cmdline +> sudo grubby --update-kernel=ALL --args="nvidia-drm.modeset=1" +> sudo systemctl enable nvidia-fallback.service +> sudo reboot +> ``` +> +> **Q: Still having issues and need help?** +> A: Please open [a support ticket](https://framework.kustomer.help/contact/support-request-ryon9uAuq). +> +> **Q: But I need a simple, reliable, tested method for pushing other applications to the dGPU? Like video track editing for example.** +> +> A: It's ready, migrating from a hidden repo soon. Works flawlessly. Coming soon! RPM installed _compatible_ software, _compatible_ Flatpaks and even wrapper handling for _compatible_ AppImages. Do note however, GPU video rendering is done with the driver (using NVENC) and for this type of function, this application would **not** be needed. + + diff --git a/framework16/Fedora-42-fw16.md b/framework16/Fedora-42-fw16.md new file mode 100644 index 0000000..09d181d --- /dev/null +++ b/framework16/Fedora-42-fw16.md @@ -0,0 +1,79 @@ +# This is for the Framework Laptop 16 (AMD Ryzen™ 7040 Series) ONLY. + +## This will: + +- Getting your laptop fully updated. +- Allow both CPU and platform drivers to be simultaneously active. +- Enable improved fractional scaling support Fedora's GNOME environment using Wayland. +- Enabling tap to click on the touchpad. + +  +  +  + +### Step 1 Updating your software packages + +- Browse to the horizontal line in the upper left corner, click to open it. +- Type out the word terminal, click to open it. +- Copy the code below in the gray box, right click/paste it into the terminal window. +- Then press the enter key, user password, enter key, **reboot.** + + +``` +sudo dnf upgrade +``` +> **TIP:** You can use the little clipboard icon to the right of the code to copy to your clipboard. + + +**Reboot** + +  +  +  + + +### Step 2 - If you want to enable fractional scaling on Wayland: + +- Browse to the horizontal line in the upper left corner, click to open it. +- Type out the word Displays. +- Look for "Scale", set it to your preference, click Apply. + +  +  +  +### Step 3 - If you want to enable "tap-to-click" on the touchpad: + +- Browse to the horizontal line in the upper left corner, click to open it. +- Type out the word mouse, look for Mouse and Touchpad, click to open it. +- Click the touchpad option at the top. +- Under "Clicking", select Tap to Click and enable it. + +  +  +  +### Bonus Step (for former Mac users) Reduce Font Scaling to Match Your Needs + +We received feedback that for users coming from OS X, installing GNOME Tweaks, browsing to Fonts, and reducing the font size from 1.00 to 0.80 may be preferred. + +- Goto Displays, set scaling to 200%. This will look too large, so let's fix the fonts. +- Install with: + +``` +sudo dnf install gnome-tweaks -y +``` + +- Open Tweaks by using the "Super" or Windows key, search tweaks, and enter. + +- At the top, select fonts. Now in that panel, scroll all the way down. Look for Size. Change from 1.00 to 0.80. Close Tweaks. + + Note: This is for the displays for the laptop only. This will look super odd on external displays and likely too large even still. + +  +  +  + +---------------------------------------- +---------------------------------------- + +  +  diff --git a/framework16/Fedora41-fw16.md b/framework16/Fedora41-fw16.md new file mode 100644 index 0000000..e5faa0e --- /dev/null +++ b/framework16/Fedora41-fw16.md @@ -0,0 +1,184 @@ +# This is for the Framework Laptop 16 (AMD Ryzen™ 7040 Series) ONLY. + +## This will: + +- Getting your laptop fully updated. +- Allow both CPU and platform drivers to be simultaneously active. +- Enable improved fractional scaling support Fedora's GNOME environment using Wayland. +- Enabling tap to click on the touchpad. + +  +  +  + +### Step 1 Updating your software packages + +- Browse to the horizontal line in the upper left corner, click to open it. +- Type out the word terminal, click to open it. +- Copy the code below in the gray box, right click/paste it into the terminal window. +- Then press the enter key, user password, enter key, **reboot.** + + +``` +sudo dnf upgrade +``` +> **TIP:** You can use the little clipboard icon to the right of the code to copy to your clipboard. + + +**Reboot** + +  +  +  + + +### Step 2 - If you want to enable fractional scaling on Wayland: + +- Browse to the horizontal line in the upper left corner, click to open it. +- Type out the word Displays. +- Look for "Scale", set it to your preference, click Apply. + +  +  +  +### Step 3 - If you want to enable "tap-to-click" on the touchpad: + +- Browse to the horizontal line in the upper left corner, click to open it. +- Type out the word mouse, look for Mouse and Touchpad, click to open it. +- Click the touchpad option at the top. +- Under "Clicking", select Tap to Click and enable it. + +  +  +  +### Bonus Step (for former Mac users) Reduce Font Scaling to Match Your Needs + +We received feedback that for users coming from OS X, installing GNOME Tweaks, browsing to Fonts, and reducing the font size from 1.00 to 0.80 may be preferred. + +- Goto Displays, set scaling to 200%. This will look too large, so let's fix the fonts. +- Install with: + +``` +sudo dnf install gnome-tweaks -y +``` + +- Open Tweaks by using the "Super" or Windows key, search tweaks, and enter. + +- At the top, select fonts. Now in that panel, scroll all the way down. Look for Size. Change from 1.00 to 0.80. Close Tweaks. + + Note: This is for the displays for the laptop only. This will look super odd on external displays and likely too large even still. + +  +  +  + +---------------------------------------- +---------------------------------------- +## Workarounds below are no longer needed + +## ~~MediaTek Bluetooth with s2idle workaround~~ +(**UPDATED:** No longer is this needed) + +- ~~[Simply visit this page](https://github.com/FrameworkComputer/linux-docs/blob/main/hibernation/kernel-6-11-workarounds/suspend-hibernate-bluetooth-workaround.md#workaround-for-suspendhibernate-black-screen-on-resume-kernel-611) (new tab), copy/paste the one liner, reboot. Now Bluetooth will stop for suspend and resume when you resume from s2idle suspend.~~ + +  +  +  + +### (No Longer Needed) USB-C Video Out from dGPU directly +(**UPDATED:** CURRENT FIRMWARE MAKES THIS UNNEEDED, JUST MAKE SURE [YOUR FIRMWARE IS CURRENT](https://guides.frame.work/Guide/Fedora+41+Installation+on+the+Framework+Laptop+16/394?lang=en#s2261). +**With the latest firmware, just connect your display**. + +~~By default, when you attach a USB-C cable to the dGPU port, it will not come out of D3cold - this is by design and is to preserve your battery life during everyday usage.~~ + +~~But you may find instances where you wish to connect to this port (HDMI/DP dongle to USB-C for example). There are a few ways to bring the dGPU out of D3cold.~~ + +- ~~[Mission Center](https://missioncenter.io/)~~ or ``lspci -v`` +- ~~Installing nvtop, then using this method.~~ + +``` +sudo dnf install nvtop +``` +~~Create a script with the following:~~ + +``` +sudo nano /usr/local/bin/external_video.sh +``` +~~Paste in:~~ + +``` +#!/bin/bash +echo "USB device connected. Running nvtop for 2 seconds." + +timeout 2 nvtop + +echo "nvtop run completed." +``` +~~Save the file. Then set it to executable.~~ + +``` +sudo chmod +x /usr/local/bin/external_video.sh +``` + +~~Now setup a udev rule.~~ +``` +sudo nano /etc/udev/rules.d/99-external_video.rules +``` + +~~Paste in.~~ + +``` +ACTION=="add", SUBSYSTEM=="usb", RUN+="/usr/local/bin/external_video.sh" +``` + +~~Save the file, then run these commands.~~ + +``sudo udevadm control --reload-rules`` +~~then~~ +``sudo udevadm trigger`` + +- ~~Plug in your adapter into the USB-C port on your dGPU port on the back, your display will come on.~~ +- ~~NOTE: If you are using HDMI, USB-C or DP explansion cards in the expansion bays on the side of the laptop, this is not needed.~~ + +  +  +  + +## (**NO LONGER NEEDED**): ~~Optional and *only if needed* - current AMD Ryzen 7040 Series workarounds to common issues~~ +(**UPDATED:** CURRENT FIRMWARE MAKES THIS UNNEEDED, JUST MAKE SURE [YOUR FIRMWARE IS CURRENT](https://guides.frame.work/Guide/Fedora+41+Installation+on+the+Framework+Laptop+16/394?lang=en#s2261). + +### ~~To prevent graphical artifacts from appearing:~~ +~~(Note, this workaround may be unneeded as it is difficult to reproduce, however, if you find you're experiencing [the issue described here](https://bugzilla.redhat.com/show_bug.cgi?id=2247154#c3), you can implement this boot parameter)~~ + + +- ~~Browse to the horizontal line in the upper left corner, click to open it.~~ +- ~~Type out the word terminal, click to open it.~~ +- ~~Then press the enter key, user password, enter key.~~ + +``` +sudo grubby --update-kernel=ALL --args="amdgpu.sg_display=0" +``` +> ~~**TIP:** You can use the little clipboard icon to the right of the code to copy to your clipboard.~~ + + +**Reboot** + +## (NO LONGER NEEDED) ~~Addtionally, we recommend the following as well if you are experiencing graphical artifacts from appearing~~ +(**UPDATED:** CURRENT FIRMWARE MAKES THIS UNNEEDED, JUST MAKE SURE [YOUR FIRMWARE IS CURRENT](https://guides.frame.work/Guide/Fedora+41+Installation+on+the+Framework+Laptop+16/394?lang=en#s2261). +**With the latest firmware, just connect your display**. + +- ~~Please follow the steps outlined in this guide: + https://knowledgebase.frame.work/allocate-additional-ram-to-igpu-framework-laptop-13-amd-ryzen-7040-series-BkpPUPQa~~ + + +---------------------------------------- + +## (NO LONGER NEEDED) ~~Framework Laptop 16 not providing all of the expected refresh rates.~~ + +~~[Framework Laptop 16 not providing all of the expected refresh rates ](https://github.com/FrameworkComputer/linux-docs/blob/main/amdgpu-workarounds/amdgpu_freesync_video/amdgpu_freesync_video.md#amdgpufreesync_video1-parameter-workaround-framework-laptop-16-only)~~ + +  +  +   +  +  diff --git a/framework16/Ubuntu-26-04-fw16-AI-300.md b/framework16/Ubuntu-26-04-fw16-AI-300.md new file mode 100644 index 0000000..87120d2 --- /dev/null +++ b/framework16/Ubuntu-26-04-fw16-AI-300.md @@ -0,0 +1,171 @@ +# Framework Laptop 16 (AMD Ryzen™ AI 300 Series) ONLY + + +## This will: + +- Get your laptop fully updated +- Install NVIDIA driver +- Install GPU monitoring tools + +  +  +  + +### Important: During Ubuntu Installation + +When installing Ubuntu, if you followed our guide, you remembered to enable **"Install third-party software for graphics and Wi-Fi hardware"** as this will automatically handle the NVIDIA driver installation for you. + +  +  +  + +### Step 1: Update System and Install CUDA Support + +- Open Terminal +- Copy the code below in the gray box, right click/paste it into the terminal window +- Then press the enter key, enter your user password, press enter key, **reboot** + +``` +sudo apt update && sudo apt upgrade -y && sudo apt install nvtop +``` + +>**Note:** ollama, text-generation-webui, vllm, oobabooga images that already bundle CUDA), so nothing needed here once your driver is installed. + +**Reboot your system after this completes** + +  +  +  + +### Step 2: Enable Fractional Scaling on Wayland (Optional) + +- Click on the top right corner, select Settings +- Navigate to "Displays" +- Look for "Scale", set it to your preference (125%, 150%, 175%, or 200%), click Apply + +  +  +  + +### Step 3: Enable "Tap-to-Click" on the Touchpad (Optional) + +- Click on the top right corner, select Settings +- Navigate to "Mouse & Touchpad" +- Under "Touchpad" section, toggle on "Tap to Click" + +### Bonus Step: Reduce Font Scaling (For Former Mac Users) + +For users coming from macOS, installing GNOME Tweaks and adjusting font scaling may provide a more familiar experience: + +- Go to Displays, set scaling to 200% (this will look too large initially) +- Install GNOME Tweaks: + +``` +sudo apt update && sudo apt install gnome-tweaks -y +``` + +- Open Tweaks by pressing the "Super" (Windows) key, search "tweaks", and press enter +- At the top, select "Fonts". Scroll down to find "Scaling Factor" +- Change from 1.00 to 0.80, then close Tweaks + +**Note:** This scaling adjustment is optimized for the laptop display only and may not look optimal on external monitors. + +  + +------------------------------------ + +## Verify NVIDIA driver installation + +`modinfo -F version nvidia` + +This will tell you your installed NVIDIA driver version. + +**Your NVIDIA driver is installed and ready for dGPU enabled Steam gaming, compute tasks, NVENC GPU rendering for video editors, etc.** + +### How to determine if your dGPU is active + +Run nvtop from the terminal. Your dGPU will be clearly labled at the top of the terminal output. You will only see activitity from nvtop for the dDPU when Steam gaming or when a workload is calling upon the dGPU to run. + +`nvtop` + + + +### Important +- We recommend using the installation method step listed under "Install third-party software for graphics and Wi-Fi hardware." Building the driver yourself or deviating from this at all whill yield varied results that are not something we tested agaist for this guide. +- When seeking support from the support team at Framework, we will be verifying you followed these directions. This is the driver handling method the support team has vetted as working and reliable. + +## Next Steps - Steam Gaming with NVIDIA + +Continue with [Gaming on Steam](https://github.com/FrameworkComputer/linux-docs/blob/main/framework16/AI-300/Gaming-on-Steam-dGPU-Ubuntu.md#gaming-on-steam-on-ubuntu) + +----------------- +  +  + +------------------------- + +## NVIDIA driver Troubleshooting + +> **Q: What if the driver failed to install?** +> +> A: If you didn't install drivers during OS install or if that failed for some reason, [check this upstream documentation](https://documentation.ubuntu.com/server/how-to/graphics/install-nvidia-drivers/) for next steps to correct this. +> +>**Q: dGPU is not doing anything or does not seem to be working?** +> A: Did you run nvidia-smi to verify you're detecting the nvidia driver? You understand that not all applications use the dGPU, even when pressed into service to do so. Browsers and other applications will not use the dGPU as there is no reason to do so. +> +> **Q: The dGPU worked previously, ran updates, now it is not working anymore, what happened?** +> A: If you installed the NVIDIA driver through Additional Drivers as recommended, check if a kernel update has occurred. You may need to reboot or reinstall the driver through Additional Drivers. If issues persist, open a support ticket as a regression may have been introduced. +> +> **Q: The NVIDIA module is installed as outlined from the dGPU installation guide, but there is question as to whether it's actually being detected at all?** +> A: From a terminal, run nvidia-smi to verify the driver is loaded. Also, you can make sure the dGPU is physically seen as present with this terminal command: +> +> `sudo lshw -C display` +> +> (The NVIDIA dGPU should appear as "product: GeForce RTX 5070 Series.") +> +> **Q: I'm using Secure Boot and the driver installation failed or isn't working properly. What should I do?** +> +> A: Ubuntu should automatically detect Secure Boot and use pre-signed kernel modules during installation. However, if this fails, follow these fallback steps: +> +> **Step 1: Completely remove all existing NVIDIA packages** +> ``` +> # First, identify installed NVIDIA packages and their driver branch numbers +> apt-mark showmanual | grep nvidia +> +> # Remove all NVIDIA packages (replace XXX with your driver branch number, e.g., 550) +> sudo apt --purge remove '*nvidia*XXX*' +> +> # Clean up any remaining dependencies +> sudo apt autoremove +> ``` +> +> **Step 2: Manually install the driver with Secure Boot support** +> +> For systems with Secure Boot enabled, use the pre-compiled signed kernel modules: +> ``` +> # First, check available drivers +> sudo ubuntu-drivers list +> +> # Install pre-compiled signed modules for your kernel +> # Replace XXX with your driver branch (e.g., 550) +> # Replace 'generic' with your kernel flavour if different +> sudo apt install linux-modules-nvidia-XXX-generic +> +> # Verify modules were installed for your current kernel +> sudo apt-cache policy linux-modules-nvidia-XXX-$(uname -r) +> +> # If not installed for current kernel, install specifically +> sudo apt install linux-modules-nvidia-XXX-$(uname -r) +> +> # Finally, install the driver metapackage +> sudo apt install nvidia-driver-XXX +> +> # Reboot your system +> sudo reboot +> ``` +> +> After reboot, verify the installation with `nvidia-smi` and `modinfo -F version nvidia`. +> + + + diff --git a/framework16/Ubuntu26.04-Setup-amd-fw16.md b/framework16/Ubuntu26.04-Setup-amd-fw16.md new file mode 100644 index 0000000..f68a2dc --- /dev/null +++ b/framework16/Ubuntu26.04-Setup-amd-fw16.md @@ -0,0 +1,138 @@ +# This is for the AMD Ryzen 7040 Series Framework Laptop 16 ONLY. + + +## This will: + +- Update your Ubuntu install's packages. +- (Optional) Stop buzzing sound from headphone jack if its present. + +        + + +### Get everything updated + +- Browse to the upper left corner, click the horizontal line to open the menu. +- Type out the word terminal, click to open it. +- Click on the small icon shown in the image below to copy the code below in the gray box, right click/paste it into the terminal window. +- Then press the enter key, user password, enter key, **reboot.** + +``` +sudo apt update && sudo apt upgrade -y && sudo snap refresh +``` +> **TIP:** You can use the little clipboard icon to the right of the code to copy to your clipboard. + +

    Copy The Code Below Like This

    + +**reboot.** + +      + +### (No Longer Needed) USB-C Video Out from dGPU directly +UPDATED: CURRENT FIRMWARE MAKES THIS UNNEEDED, JUST MAKE SURE [YOUR FIRMWARE IS CURRENT](https://guides.frame.work/Guide/Fedora+41+Installation+on+the+Framework+Laptop+16/394?lang=en#s2261). +**With the latest firmware, just connect your display**. + +By default, when you attach a USB-C cable to the dGPU port, it will not come out of [D3cold](https://learn.microsoft.com/en-us/windows-hardware/drivers/kernel/device-power-states) - this is by design and is to preserve your battery life during everyday usage. + +But you may find instances where you wish to connect to this port (HDMI/DP dongle to USB-C for example). There are a few ways to bring the dGPU out of D3cold. + +- [Mission Center](https://missioncenter.io/) or ``lspci -v`` +- Installing nvtop, then using this method. + +``` +sudo apt update && sudo apt install nvtop -y +``` +Create a script with the following: + +``` +sudo nano /usr/local/bin/external_video.sh +``` +Paste in: + +``` +#!/bin/bash +echo "USB device connected. Running nvtop for 2 seconds." + +timeout 2 nvtop + +echo "nvtop run completed." +``` + +Save the file. Then set it to executable. + +``` +sudo chmod +x /usr/local/bin/external_video.sh +``` + +Now setup a udev rule. +``` +sudo nano /etc/udev/rules.d/99-external_video.rules +``` + +Paste in. + +``` +ACTION=="add", SUBSYSTEM=="usb", RUN+="/usr/local/bin/external_video.sh" +``` + +Save the file, then run these commands. + +``sudo udevadm control --reload-rules`` +then +``sudo udevadm trigger`` + +- Plug in your adapter into the USB-C port on your dGPU port on the back, your display will come on. +- NOTE: If you are using HDMI, USB-C or DP explansion cards in the expansion bays on the side of the laptop, this is not needed. + +  +  +  + +### Optional and only if needed - current AMD Ryzen 7040 Series workarounds to common issues + +### Buzzing sound from headphone jack + +- Browse to the horizontal line in the upper left corner, click to open it. +- Type out the word terminal, click to open it. +- Copy/paste in the following code below. +- Press the enter key, user password, enter key. + +``` +echo 0 | sudo tee /sys/module/snd_hda_intel/parameters/power_save +``` +> **TIP:** You can use the little clipboard icon to the right of the code to copy to your clipboard. + + +Then: + +**Reboot** + +  + +### Bonus Step (for former Mac users) Reduce Font Scaling to Match Your Needs + +We received feedback that for users coming from macOS, that installing GNOME Tweaks, browsing to Fonts, and reducing the font size from 1.00 to 0.80 may be preferred. + +- Goto Displays, set scaling to 200%. This will look too large, so let's fix the fonts. +- Install GNOME Tweaks either by searching for it in Ubuntu Software or on the terminal with: + +``` +sudo apt update && sudo apt install gnome-tweaks -y +``` + +- Open Tweaks by using the "Super" or Windows key, search tweaks, and enter. + +- At the top, select fonts. Now in that panel, scroll all the way down. Look for Size. Change from 1.00 to 0.80. Close Tweaks. + + Note: This is for the displays for the laptop only. This will look super odd on external displays and likely too large even still. + +  +  +  + +---------------------------------------- + +  +  +   +  +  diff --git a/framework16/framework16-7040-series-nixos.md b/framework16/framework16-7040-series-nixos.md new file mode 100644 index 0000000..ed0e51c --- /dev/null +++ b/framework16/framework16-7040-series-nixos.md @@ -0,0 +1,52 @@ +# Adding the NixOS-Hardware Module (Framework Laptop 16, AMD Ryzen 7040 Series) + +This provides the hardware channel steps for the NixOS on the Framework Laptop 16 Guide. + +It is recommended to use `power-profiles-daemon` over `tlp` for the AMD Framework 16. + +## What this module does + +- Imports the common AMD and Raphael iGPU hardware configuration +- Enables `services.fwupd.enable`, so firmware updates are handled automatically +- Enables `services.fprintd.enable`, for fingerprint reader support +- Enables `hardware.keyboard.qmk.enable` and adds a libinput quirks override, for the Framework 16 keyboard module +- Enables `hardware.sensor.iio.enable`, needed for desktop environments to detect and manage display brightness +- Adds a udev rule fixing USB autosuspend on the Ethernet expansion card + +## Channel-based (default from graphical installer) + +```bash +sudo nix-channel --add https://github.com/NixOS/nixos-hardware/archive/master.tar.gz nixos-hardware +sudo nix-channel --update +``` + +```nix +# /etc/nixos/configuration.nix +imports = [ + ./hardware-configuration.nix + +]; +``` + +```bash +sudo nixos-rebuild switch +``` + +## Flake-Based + +```nix +# flake.nix inputs +inputs.nixos-hardware.url = "github:NixOS/nixos-hardware/master"; +``` + +```nix +# flake.nix modules list +modules = [ + ./configuration.nix + nixos-hardware.nixosModules.framework-16-7040-amd +]; +``` + +```bash +sudo nixos-rebuild switch --flake .# +``` diff --git a/fw-log-tool/README.md b/fw-log-tool/README.md new file mode 100644 index 0000000..814c4c0 --- /dev/null +++ b/fw-log-tool/README.md @@ -0,0 +1,90 @@ +# Framework Log Gathering Tool + +Collects hardware facts and system state from Framework laptops and desktops running Linux. + +> ** Your diagnostic report contains sensitive system data** — IP addresses, WiFi network names, VPN connections, DNS servers, kernel boot parameters, and full system logs. **Review the output before sharing it publicly** (e.g. forum posts, GitHub issues). Redact anything you're not comfortable posting. + +> ** This tool auto-installs missing dependencies** — packages like `lspci`, `dmidecode`, and `lm-sensors` are installed via your distro's package manager (`apt`, `dnf`, `pacman`, `zypper`) **without prompting**. Dependencies come from your distro's official repositories. If [`framework_tool`](https://github.com/FrameworkComputer/framework-system) is not found locally, it is downloaded from GitHub and run as root, then deleted. On immutable distros (Bluefin, Bazzite, etc.) package installation is skipped because the package manager can't be used directly — if tools are missing, results may be incomplete. +> + +## Log Gathering Tool not working **or** prefer a manual approach instead? + +Paste this single-line command instead, then press Enter: + +```echo "Saving logs to logs_24h.txt..."; (echo "== DMESG for Last 24 Hours =="; sudo journalctl -k --since="24 hours ago"; echo "== JOURNALCTL for Last 24 Hours =="; sudo journalctl --since="24 hours ago") > logs_24h.txt``` + +A file named logs_24h.txt will appear in your current directory. Attach that file in your reply to support. + +----------------------------------- +----------------------------------- +----------------------------------- + +## Log Gathering Tool Quick Start + +```bash +curl -sO https://raw.githubusercontent.com/FrameworkComputer/linux-docs/main/fw-log-tool/fw_diag.pyz && chmod +x fw_diag.pyz +sudo ./fw_diag.pyz +``` + +First line downloads the tool and makes it executable. Second line runs it. No wrapper script, no pip install, no venv — just Python 3. + +### Other run modes + +```bash +sudo ./fw_diag.pyz --since boot # Since last boot +sudo ./fw_diag.pyz --since 24h # Last 24 hours +sudo ./fw_diag.pyz -o my_report.txt # Custom output file +sudo ./fw_diag.pyz --json # Structured JSON output +``` + +`sudo` is recommended. Without it, some sections (dmidecode, dmesg, NVMe SMART) will be incomplete. + +## What It Collects + +- **System info** — kernel version, distro, desktop environment, session type (Wayland/X11), power management daemon (ppd/tuned/TLP) with active profile and conflict detection +- **Hardware** — GPU (vendor, driver, iGPU/dGPU classification), NVMe drives, WiFi adapter, RAM (type, speed), expansion cards with USB port mapping, webcam, mic capture devices, connected displays (connector, resolution, refresh rate, PSR status), RF kill state +- **Battery** — level, health %, cycle count, charge rate, charge limit +- **Disk health** — NVMe health via `nvme smart-log`, SATA health via `smartctl` +- **Thermal** — current temps with AMD/Intel-specific thresholds (modern AMD runs hotter by design) +- **Network** — internet/WiFi/ethernet connectivity, VPN connections, IP addresses, DNS servers, WiFi power save state +- **Audio** — PipeWire/PulseAudio server info, default output/input devices, mute/volume state, ALSA mixer levels +- **Bluetooth** — adapter power/block state, connected and paired devices +- **Sleep/suspend** — current mode (s2idle/deep), available modes, ACPI states, kernel suspend stats, suspend/resume cycle counts +- **Firmware** — BIOS/EC versions, Secure Boot status (mokutil → EFI variable → bootctl fallback), Thunderbolt firmware version, fwupd devices and update status, fingerprint reader (driver, fprintd service, enrollment, PAM config). If [`framework_tool`](https://github.com/FrameworkComputer/framework-system) is not found locally, downloads it from GitHub, runs `--versions` and `--pd-info`, then deletes the downloaded binary. +- **Distro compatibility** — checks your distro/version against the Framework support matrix +- **FW12-specific** — tablet mode, screen rotation, touchscreen/stylus (Framework Laptop 12 only) +- **Raw logs** — journalctl for the selected time range + full dmesg ring buffer (not time-filtered — captures early boot messages journald may miss), plus a log summary that flags critical events (GPU errors, kernel panics, NVMe I/O errors, filesystem errors) with confidence levels + +Output lands in `diagnostic_output.txt` (or `.json` with `--json`). + +## Supported Framework Devices + +- Framework Laptop 13 (11th–13th Gen Intel, Intel Core Ultra, AMD Ryzen 7040, AMD Ryzen AI 300) +- Framework Laptop 16 (AMD Ryzen 7040, AMD Ryzen AI 300) +- Framework Laptop 12 +- Framework Desktop (AMD Ryzen AI Max 300) + +## Distro Support + +The supported distro versions listed here reflect the current compatibility matrix and will be updated as new releases come out. The tool itself works on beta, pre-release, and development versions of these distros — data collection doesn't depend on a specific release version. + +**Officially supported** (per [frame.work/linux](https://frame.work/linux)): +- Fedora 43 +- Ubuntu (version depends on model — 25.10 for newest hardware, 24.04+ or 22.04 for older models) +- Bazzite + +**Community supported** — the tool runs and auto-installs deps on: +- Arch, CachyOS, EndeavourOS, Garuda +- Debian, Pop!_OS, elementary OS, Zorin +- openSUSE Tumbleweed/Leap +- NixOS (required tools like `lspci`, `dmidecode`, `sensors` aren't in PATH by default — the tool re-execs itself inside `nix-shell -p` with them, nothing permanently installed) +- Bluefin, Aurora, Kinoite, Silverblue (immutable — auto-install skipped, required tools must already be present) +- Linux Mint (not on AI 300 or Desktop models) +- Manjaro (11th/12th Gen Intel only) + +Auto-install uses `apt`, `dnf`, `pacman`, or `zypper` depending on your distro. + +## Requirements + +- Python 3 (no third-party Python packages needed) +- Standard Linux CLI tools (`lspci`, `dmidecode`, `sensors`, `iw`, `nvme`, `fwupdmgr`, etc.) — installed automatically if missing diff --git a/fw-log-tool/__main__.py b/fw-log-tool/__main__.py new file mode 100644 index 0000000..348be07 --- /dev/null +++ b/fw-log-tool/__main__.py @@ -0,0 +1,2 @@ +from framework_diagnostic.__main__ import main +main() diff --git a/fw-log-tool/framework_diagnostic/README.md b/fw-log-tool/framework_diagnostic/README.md new file mode 100644 index 0000000..475fc9b --- /dev/null +++ b/fw-log-tool/framework_diagnostic/README.md @@ -0,0 +1,24 @@ +# framework_diagnostic/ + +Source modules for `fw_diag.pyz`. This is what's inside the zip. + +## Modules + +| File | What it does | +|---|---| +| `__main__.py` | Entry point — argparse, interactive menu, report assembly, JSON output | +| `hardware.py` | GPU, NVMe, WiFi, RAM, expansion cards, webcam, mic, displays, RF kill, battery, disk health (nvme-cli + smartctl) | +| `firmware.py` | fwupd devices, BIOS/EC versions, Secure Boot (mokutil - EFI var - bootctl), Thunderbolt FW, fingerprint reader, kernel cmdline, `framework_tool --versions` / `--pd-info` | +| `system_info.py` | Kernel version, distro, desktop environment, session type (Wayland/X11), power management daemon (ppd/tuned/TLP), active profile, conflict detection | +| `thermal.py` | CPU/GPU/NVMe temps via `sensors`, AMD/Intel-specific thresholds | +| `network.py` | Internet/WiFi/ethernet connectivity, IP addresses, DNS, VPN detection, WiFi power save | +| `audio.py` | PipeWire vs PulseAudio, session manager, default sink/source, mute/volume at server and ALSA level | +| `bluetooth.py` | Adapter status, connected/paired devices, rfkill soft/hard block | +| `sleep.py` | Sleep mode (s2idle/deep), ACPI states, kernel suspend stats, suspend/resume counts, inhibitors, resume errors | +| `log_summary.py` | Scans journalctl + dmesg for critical events (GPU errors, kernel panics, NVMe I/O errors, OOM kills, etc.) with confidence levels | +| `distro_compat.py` | Checks running distro/version against Framework's per-model support matrix | +| `fw12.py` | Framework Laptop 12 only — tablet mode, screen rotation, touchscreen/stylus | +| `dependencies.py` | Checks for required CLI tools, auto-installs via apt/dnf/pacman/zypper, NixOS nix-shell re-exec, immutable distro handling | +| `output.py` | ANSI colors, progress display, report builder | +| `utils.py` | `run_command()` and `run_sudo_command()` wrappers used by all modules | +| `__init__.py` | Version (`5.3.0`), public API exports | diff --git a/fw-log-tool/framework_diagnostic/__init__.py b/fw-log-tool/framework_diagnostic/__init__.py new file mode 100644 index 0000000..01df98f --- /dev/null +++ b/fw-log-tool/framework_diagnostic/__init__.py @@ -0,0 +1,61 @@ +""" +Framework Diagnostic Tool - Data Collection Only + +A diagnostic data collection tool for Framework laptops and desktops running Linux. +This tool collects hardware facts and system state - it does NOT analyze logs +for issues. Use fw_triage.py for issue detection. + +Features: +- Hardware detection (GPU, NVMe, WiFi, RAM, expansion cards) +- Firmware status (fwupd devices, BIOS version, EC version, fingerprint reader) +- Thermal monitoring with AMD/Intel-specific thresholds +- Network connectivity checking +- Sleep/suspend status (sysfs readings) +- Distribution compatibility checking +- System information (kernel, desktop, distro, kernel cmdline, Secure Boot) +- Raw log collection for external analysis +- JSON output for programmatic consumption by fw_triage + +Usage: + python -m framework_diagnostic # Interactive menu + python -m framework_diagnostic --since boot # Since last boot + python -m framework_diagnostic --json # Structured JSON output + python -m framework_diagnostic --output report.txt +""" + +__version__ = '5.3.0' # Battery detail, bluetooth, power conflict detection, expansion card USB topology +__author__ = 'Framework Diagnostic Contributors' + +from .hardware import detect_all_hardware, HardwareInfo +from .thermal import check_current_temperatures, ThermalInfo +from .network import check_network_connectivity, NetworkStatus +from .sleep import check_sleep_status, SleepStatus +from .distro_compat import check_framework_distro_compatibility, CompatibilityResult +from .system_info import detect_system_info, SystemInfo +from .firmware import detect_firmware_info, FirmwareInfo +from .fw12 import detect_fw12_diagnostics, FW12Diagnostics +from .audio import detect_audio, AudioInfo +from .bluetooth import detect_bluetooth, BluetoothInfo + +__all__ = [ + 'detect_all_hardware', + 'HardwareInfo', + 'check_current_temperatures', + 'ThermalInfo', + 'check_network_connectivity', + 'NetworkStatus', + 'check_sleep_status', + 'SleepStatus', + 'check_framework_distro_compatibility', + 'CompatibilityResult', + 'detect_system_info', + 'SystemInfo', + 'detect_firmware_info', + 'FirmwareInfo', + 'detect_fw12_diagnostics', + 'FW12Diagnostics', + 'detect_audio', + 'AudioInfo', + 'detect_bluetooth', + 'BluetoothInfo', +] diff --git a/fw-log-tool/framework_diagnostic/__main__.py b/fw-log-tool/framework_diagnostic/__main__.py new file mode 100644 index 0000000..f02c17a --- /dev/null +++ b/fw-log-tool/framework_diagnostic/__main__.py @@ -0,0 +1,717 @@ +#!/usr/bin/env python3 +""" +Framework Diagnostic Tool - Data Collection Only + +Collects hardware facts, thermal status, network status, sleep configuration, +and raw logs. Does NOT analyze logs for issues - use fw_triage.py for that. + +Usage: + python -m framework_diagnostic # Interactive menu + python -m framework_diagnostic --since boot # Since last boot + python -m framework_diagnostic --since 24h # Last 24 hours + python -m framework_diagnostic -o report.txt # Custom output file +""" + +import argparse +import sys +import os +import json +import subprocess +from dataclasses import dataclass, fields, is_dataclass +from datetime import datetime, timedelta +from pathlib import Path +from typing import Optional +from enum import Enum + +from .output import ( + print_colored, print_error, print_success, print_info, + print_warning, show_progress, Color, ReportBuilder +) +from .hardware import detect_all_hardware, format_hardware_report, format_disk_health_report +from .thermal import check_current_temperatures, format_thermal_report +from .network import check_network_connectivity, format_network_report +from .distro_compat import check_framework_distro_compatibility, format_compatibility_report +from .dependencies import ensure_dependencies +from .sleep import check_sleep_status, format_sleep_status_report +from .system_info import detect_system_info, format_system_info_report +from .firmware import detect_firmware_info, format_firmware_report +from .log_summary import extract_activity, format_activity +from .fw12 import detect_fw12_diagnostics, format_fw12_report +from .audio import detect_audio, format_audio_report +from .bluetooth import detect_bluetooth, format_bluetooth_report + + +def _serialize(obj): + """Serialize dataclasses/enums to JSON-safe dicts.""" + if is_dataclass(obj) and not isinstance(obj, type): + return {f.name: _serialize(getattr(obj, f.name)) for f in fields(obj)} + if isinstance(obj, Enum): + return obj.value + if isinstance(obj, list): + return [_serialize(item) for item in obj] + if isinstance(obj, dict): + return {k: _serialize(v) for k, v in obj.items()} + if isinstance(obj, Path): + return str(obj) + return obj + + +def check_root(): + """Check if running with root privileges (needed for some operations).""" + if os.geteuid() != 0: + print_info("Some diagnostics require root privileges.") + print_info("Consider running with: sudo python -m framework_diagnostic") + return False + return True + + +def get_boot_time() -> Optional[datetime]: + """Get actual system boot time.""" + try: + # Method 1: Parse /proc/uptime + with open('/proc/uptime', 'r') as f: + uptime_seconds = float(f.read().split()[0]) + return datetime.now() - timedelta(seconds=uptime_seconds) + except Exception: + pass + + try: + # Method 2: Use 'who -b' command + result = subprocess.run(['who', '-b'], capture_output=True, text=True, timeout=5) + if result.returncode == 0: + # Output like: " system boot 2024-01-15 08:30" + parts = result.stdout.strip().split() + if len(parts) >= 4: + date_str = f"{parts[-2]} {parts[-1]}" + return datetime.strptime(date_str, '%Y-%m-%d %H:%M') + except Exception: + pass + + return None + + +def get_time_range(choice: int) -> tuple[str, str]: + """Get start and end time based on menu choice.""" + now = datetime.now() + + if choice == 1: + # Last boot - get actual boot time + boot_time = get_boot_time() + if boot_time: + start = boot_time + else: + # Fallback: use journalctl -b behavior (will be handled by journalctl itself) + start = now - timedelta(days=1) # Safe fallback + end = now + elif choice == 2: + # Last 24 hours + start = now - timedelta(hours=24) + end = now + else: + # Default to last boot + boot_time = get_boot_time() + if boot_time: + start = boot_time + else: + start = now - timedelta(days=1) + end = now + + return start.strftime('%Y-%m-%d %H:%M'), end.strftime('%Y-%m-%d %H:%M') + + +def interactive_menu() -> tuple[int, Optional[str], Optional[str], Optional[int]]: + """Display interactive menu and get user choice.""" + print() + print_colored("Framework Diagnostic Tool", Color.CYAN, bold=True) + print_colored("=" * 40, Color.CYAN) + print() + print("Choose analysis time range:") + print(" 1. Last X minutes") + print(" 2. Last 24 hours") + print(" 3. Custom time range") + print() + + try: + choice = int(input("Enter choice (1-3): ")) + except (ValueError, KeyboardInterrupt): + print() + return 0, None, None, None + + if choice == 1: + print() + try: + minutes = int(input("Enter number of minutes: ")) + now = datetime.now() + start_time = (now - timedelta(minutes=minutes)).strftime('%Y-%m-%d %H:%M') + end_time = now.strftime('%Y-%m-%d %H:%M') + return choice, start_time, end_time, minutes + except (ValueError, KeyboardInterrupt): + print() + return 0, None, None, None + elif choice == 2: + start_time, end_time = get_time_range(2) + return choice, start_time, end_time, None + elif choice == 3: + print() + start_time = input("Enter start time (YYYY-MM-DD HH:MM): ") + end_time = input("Enter end time (YYYY-MM-DD HH:MM): ") + return choice, start_time, end_time, None + else: + return choice, None, None, None + + +def get_dmesg_output() -> str: + """Get kernel ring buffer via dmesg. + + This is a different data source from journalctl — the ring buffer can + have early boot messages that journald missed because it wasn't running yet. + Timestamps stripped: kernel timestamps are unreliable after suspend cycles. + Returns the entire ring buffer (no time filtering available). + """ + try: + result = subprocess.run( + ['sudo', 'dmesg', '--notime'], + capture_output=True, text=True, timeout=30 + ) + if result.returncode == 0: + return result.stdout + except (subprocess.TimeoutExpired, FileNotFoundError): + pass + return "" + + +def get_journalctl_output(since: str, until: str) -> str: + """Get journalctl output for a time range.""" + try: + result = subprocess.run( + ['sudo', 'journalctl', '--no-pager', f'--since={since}', f'--until={until}'], + capture_output=True, text=True, timeout=60 + ) + if result.returncode == 0: + return result.stdout + except (subprocess.TimeoutExpired, FileNotFoundError): + pass + return "" + + +def run_diagnostics( + start_time: str, + end_time: str, + output_file: str = "diagnostic_output.txt", +) -> int: + """ + Run diagnostic data collection. + + Collects hardware facts, thermal status, network, sleep configuration, + and logs with key event callouts. Does NOT analyze logs for issues. + + Returns: + Exit code (0 = success) + """ + report = ReportBuilder() + + # Header + report.add_line("=" * 60) + report.add_line("FRAMEWORK DIAGNOSTIC REPORT") + report.add_line(f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") + report.add_line(f"Time Range: {start_time} to {end_time}") + report.add_line("=" * 60) + report.add_line() + report.add_line("NOTE: This report contains diagnostic DATA only.") + report.add_line() + + # System information + print_info("Detecting system information...") + sys_info = detect_system_info() + for line in format_system_info_report(sys_info): + report.add_line(line) + report.add_line() + + # Hardware detection + print_info("Detecting hardware...") + hw = detect_all_hardware() + + # Framework info + if hw.framework.is_framework: + report.add_line("Framework Device:") + report.add_line(f" Product: {hw.framework.product_name}") + report.add_line(f" Model: {hw.framework.model_version}") + if hw.framework.model_type: + report.add_line(f" Type: {hw.framework.model_type}") + report.add_line(f" BIOS: {hw.framework.bios_version}") + + # Power status + report.add_line(" Power Status:") + ac_str = "Connected" if hw.framework.ac_connected else "Disconnected" + report.add_line(f" AC Power: {ac_str}") + + if hw.framework.battery_level is not None: + bat_str = f"{hw.framework.battery_level}% ({hw.framework.battery_status})" + report.add_line(f" Battery: {bat_str}") + if hw.framework.battery_health_pct is not None: + health_icon = "✅" if hw.framework.battery_health_pct >= 80 else "⚠️" if hw.framework.battery_health_pct >= 60 else "❌" + cap_str = "" + if hw.framework.battery_full_wh and hw.framework.battery_design_wh: + cap_str = f" ({hw.framework.battery_full_wh} / {hw.framework.battery_design_wh} Wh)" + report.add_line(f" Battery Health: {health_icon} {hw.framework.battery_health_pct}% of design capacity{cap_str}") + if hw.framework.battery_cycle_count is not None: + report.add_line(f" Cycle Count: {hw.framework.battery_cycle_count}") + if hw.framework.battery_charge_rate_w is not None and hw.framework.battery_charge_rate_w > 0: + direction = "charging" if hw.framework.battery_status in ("Charging", "Full") else "discharging" + report.add_line(f" Power Draw: {hw.framework.battery_charge_rate_w} W ({direction})") + if hw.framework.battery_charge_limit_pct is not None: + report.add_line(f" Charge Limit: {hw.framework.battery_charge_limit_pct}%") + + if hw.framework.expansion_cards: + if hw.framework.expansion_card_ports: + report.add_line(" Expansion Cards:") + for card_name, port_path in hw.framework.expansion_card_ports: + report.add_line(f" {card_name} (USB port: {port_path})") + # List any cards that didn't get a port mapping + mapped_names = {name for name, _ in hw.framework.expansion_card_ports} + for card in hw.framework.expansion_cards: + if card not in mapped_names: + report.add_line(f" {card}") + else: + report.add_line(f" Expansion Cards: {', '.join(hw.framework.expansion_cards)}") + else: + report.add_line(" Expansion Cards: None detected (USB-A/USB-C cards are passive and invisible to software)") + + report.add_line() + + # Hardware context + for line in format_hardware_report(hw): + report.add_line(line) + report.add_line() + + # Disk health + disk_health_lines = format_disk_health_report(hw) + if disk_health_lines: + for line in disk_health_lines: + report.add_line(line) + report.add_line() + + # Thermal check + print_info("Checking temperatures...") + thermal = check_current_temperatures( + hw.cpu_vendor, hw.amd_generation, hw.framework.is_framework + ) + for line in format_thermal_report(thermal, hw.cpu_vendor, hw.amd_generation, hw.framework.is_framework): + report.add_line(line) + report.add_line() + + # Network check + print_info("Checking network connectivity...") + network = check_network_connectivity() + for line in format_network_report(network): + report.add_line(line) + report.add_line() + + # Audio check + print_info("Checking audio configuration...") + audio = detect_audio() + audio_lines = format_audio_report(audio) + for line in audio_lines: + report.add_line(line) + report.add_line() + + # Bluetooth check + print_info("Checking bluetooth...") + bt = detect_bluetooth() + # Cross-reference rfkill state from hardware detection + for rfdev in hw.rfkill_devices: + if rfdev.device_type.lower() == 'bluetooth': + bt.soft_blocked = rfdev.soft_blocked + bt.hard_blocked = rfdev.hard_blocked + break + for line in format_bluetooth_report(bt): + report.add_line(line) + report.add_line() + + # Sleep status + print_info("Checking sleep/suspend status...") + sleep_status = check_sleep_status() + for line in format_sleep_status_report(sleep_status): + report.add_line(line) + report.add_line() + + # Firmware status + print_info("Checking firmware status...") + firmware = detect_firmware_info( + bios_version=hw.framework.bios_version, + is_framework=hw.framework.is_framework + ) + for line in format_firmware_report(firmware): + report.add_line(line) + report.add_line() + + # Distro compatibility + if hw.framework.is_framework: + print_info("Checking distribution compatibility...") + compat = check_framework_distro_compatibility( + hw.framework.product_name, + hw.framework.model_version, + hw.cpu_model + ) + if compat: + for line in format_compatibility_report(compat): + report.add_line(line) + report.add_line() + + # Framework 12 specific diagnostics + if hw.framework.is_framework: + fw12_diag = detect_fw12_diagnostics(hw.framework.model_type, sys_info.desktop_environment) + if fw12_diag.is_fw12: + print_info("Running Framework 12 diagnostics...") + fw12_lines = format_fw12_report(fw12_diag) + for line in fw12_lines: + report.add_line(line) + report.add_line() + + # Collect logs + print_info("Collecting system logs...") + dmesg_output = get_dmesg_output() + journal_output = get_journalctl_output(start_time, end_time) + + # Scan logs for activity summary. + # Journalctl is primary (has systemd + kernel, proper timestamps). + # Dmesg is secondary — the ring buffer covers the entire boot, so it + # catches critical errors and suspend cycles that fall outside the + # journalctl time window. We merge dmesg findings into the journalctl + # results to avoid the double-counting bug while still detecting + # everything in the ring buffer. + if journal_output or dmesg_output: + print_info("Scanning logs for activity summary...") + + # Primary scan: journalctl (lifecycle events, service failures) + activity = extract_activity(journal_output) if journal_output else extract_activity('') + + # Secondary scan: dmesg (critical checks, suspend cycles) + if dmesg_output: + dmesg_activity = extract_activity(dmesg_output) + + # Merge critical findings from dmesg that journalctl missed + journal_found_labels = {label for label, _ in activity.critical_found} + for label, conf in dmesg_activity.critical_found: + if label not in journal_found_labels: + activity.critical_found.append((label, conf)) + activity.critical_clear = [ + (l, c) for l, c in activity.critical_clear if l != label + ] + + # Use dmesg suspend cycles if journalctl had fewer + # (dmesg ring buffer covers entire boot, journalctl is time-windowed) + if len(dmesg_activity.suspend_cycles) > len(activity.suspend_cycles): + activity.suspend_cycles = dmesg_activity.suspend_cycles + + # Merge xHCI workaround status from dmesg + if dmesg_activity.xhci_hc_died: + activity.xhci_hc_died = True + activity.xhci_fix_installed = dmesg_activity.xhci_fix_installed + activity.xhci_fix_service_enabled = dmesg_activity.xhci_fix_service_enabled + + show_progress(100, "Log scan complete") + + for line in format_activity(activity, time_range=f'{start_time} to {end_time}'): + report.add_line(line) + + # Output each log source as its own section, unmodified + if journal_output: + report.add_line("=" * 60) + report.add_line(f"JOURNALCTL ({start_time} to {end_time})") + report.add_line("=" * 60) + report.add_line() + report.add_line(journal_output) + report.add_line() + + if dmesg_output: + report.add_line("=" * 60) + report.add_line("DMESG (kernel ring buffer — ENTIRE buffer, no time filtering)") + report.add_line("NOTE: Timestamps stripped due to kernel bug causing incorrect") + report.add_line("dates after suspend/resume cycles. Use journalctl above for") + report.add_line("time-accurate logs.") + report.add_line("=" * 60) + report.add_line() + report.add_line(dmesg_output) + report.add_line() + + if not journal_output and not dmesg_output: + report.add_line("=" * 60) + report.add_line("SYSTEM LOGS") + report.add_line("=" * 60) + report.add_line() + report.add_line(" (no log content available — are you running with sudo?)") + report.add_line() + + # Write report + output_path = Path(output_file) + with open(str(output_path), 'w') as f: + f.write(report.get_content()) + + # Print summary + print() + print_success("Diagnostic collection complete!") + print_colored(f"📋 Report saved to: {output_path.absolute()}", Color.CYAN, bold=True) + + # Quick summary + print() + print_colored("Quick Summary:", Color.CYAN, bold=True) + + if hw.framework.is_framework: + print_colored(f" 🖥️ {hw.framework.product_name}", Color.GREEN) + + print_colored(f" 🐧 Kernel: {sys_info.kernel_version}", Color.BLUE) + + if sys_info.desktop_environment: + print_colored(f" 🖥️ Desktop: {sys_info.desktop_environment} ({sys_info.session_type})", Color.BLUE) + + if thermal.cpu_temp: + print_colored(f" 🌡️ CPU Temp: {thermal.cpu_temp}°C", Color.BLUE) + + if sleep_status.current_mode.value == 's2idle': + print_colored(" 😴 Sleep: s2idle (modern standby) ✅", Color.GREEN) + else: + print_colored(f" 😴 Sleep: {sleep_status.current_mode.value}", Color.YELLOW) + + if network.internet_working: + print_colored(" 🌐 Internet: Connected ✅", Color.GREEN) + else: + print_colored(" 🌐 Internet: Not connected ❌", Color.RED) + + # BIOS version + if firmware.bios_version: + print_colored(f" 💾 BIOS: {firmware.bios_version}", Color.BLUE) + + # Firmware updates + if firmware.updates_available > 0: + print_colored(f" 📦 {firmware.updates_available} firmware update(s) available", Color.YELLOW) + + # Fingerprint + if firmware.fingerprint.detected: + print_colored(f" 👆 Fingerprint: {firmware.fingerprint.model}", Color.BLUE) + + # Next steps + print() + print_colored("Next Steps:", Color.CYAN, bold=True) + print(f" Locate \"{output_path}\" in the directory you downloaded this script into,") + print(f" send the {output_path.name} to support for your support ticket.") + + if hw.framework.is_framework: + print() + print_colored("Framework Resources:", Color.CYAN, bold=True) + print(f" Support: https://frame.work/support") + print(f" Community: https://community.frame.work/") + print(f" Linux Docs: https://github.com/FrameworkComputer/linux-docs") + + return 0 + + +def run_json_diagnostics( + start_time: str, + end_time: str, + output_file: str = "diagnostic_output.json" +) -> int: + """ + Run diagnostic collection and output structured JSON. + + Same data collection as run_diagnostics but outputs a JSON file + that fw_triage or other tools can consume programmatically without + re-running hardware detection commands. + + Returns: + Exit code (0 = success) + """ + data = { + 'meta': { + 'generated': datetime.now().isoformat(), + 'time_range': {'start': start_time, 'end': end_time}, + 'tool_version': '5.3.0', + } + } + + # System info + print_info("Detecting system information...") + sys_info = detect_system_info() + data['system_info'] = _serialize(sys_info) + + # Hardware + print_info("Detecting hardware...") + hw = detect_all_hardware() + data['hardware'] = _serialize(hw) + + # Thermal + print_info("Checking temperatures...") + thermal = check_current_temperatures( + hw.cpu_vendor, hw.amd_generation, hw.framework.is_framework + ) + data['thermal'] = _serialize(thermal) + + # Network + print_info("Checking network...") + network = check_network_connectivity() + data['network'] = _serialize(network) + + # Audio + print_info("Checking audio...") + audio = detect_audio() + data['audio'] = _serialize(audio) + + # Bluetooth + print_info("Checking bluetooth...") + bt = detect_bluetooth() + for rfdev in hw.rfkill_devices: + if rfdev.device_type.lower() == 'bluetooth': + bt.soft_blocked = rfdev.soft_blocked + bt.hard_blocked = rfdev.hard_blocked + break + data['bluetooth'] = _serialize(bt) + + # Sleep + print_info("Checking sleep status...") + sleep_status = check_sleep_status() + data['sleep'] = _serialize(sleep_status) + + # Firmware + print_info("Checking firmware...") + firmware = detect_firmware_info( + bios_version=hw.framework.bios_version, + is_framework=hw.framework.is_framework + ) + data['firmware'] = _serialize(firmware) + + # Distro compat + if hw.framework.is_framework: + compat = check_framework_distro_compatibility( + hw.framework.product_name, + hw.framework.model_version, + hw.cpu_model + ) + if compat: + data['distro_compatibility'] = _serialize(compat) + + # FW12 diagnostics + if hw.framework.is_framework: + fw12_diag = detect_fw12_diagnostics(hw.framework.model_type, sys_info.desktop_environment) + if fw12_diag.is_fw12: + data['fw12_diagnostics'] = _serialize(fw12_diag) + + # Raw logs + print_info("Collecting logs...") + dmesg_output = get_dmesg_output() + journal_output = get_journalctl_output(start_time, end_time) + data['logs'] = { + 'dmesg': dmesg_output if dmesg_output else None, + 'journalctl': journal_output if journal_output else None, + } + + # Write JSON + output_path = Path(output_file) + with open(str(output_path), 'w') as f: + json.dump(data, f, indent=2, default=str) + + print() + print_success("Diagnostic collection complete!") + print_colored(f"📋 JSON report saved to: {output_path.absolute()}", Color.CYAN, bold=True) + print_info("This file can be consumed by fw_triage.py or other analysis tools.") + + return 0 + + +def main(): + """Main entry point.""" + parser = argparse.ArgumentParser( + description="Framework Diagnostic Tool - Data Collection", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +This tool collects diagnostic data. It does NOT analyze logs for issues. +For issue detection, use fw_triage.py on the generated output. + +Examples: + python -m framework_diagnostic Interactive menu + python -m framework_diagnostic --since boot Since last boot + python -m framework_diagnostic --since 24h Last 24 hours + python -m framework_diagnostic -o report.txt Custom output file + """ + ) + + parser.add_argument( + '--since', + choices=['boot', '24h'], + help='Time range for log collection' + ) + + parser.add_argument( + '--output', '-o', + default='diagnostic_output.txt', + help='Output file path (default: diagnostic_output.txt)' + ) + + parser.add_argument( + '--quiet', '-q', + action='store_true', + help='Minimal output' + ) + + parser.add_argument( + '--json', + action='store_true', + help='Output structured JSON (for consumption by fw_triage or other tools)' + ) + + args = parser.parse_args() + + # Check root + check_root() + + # Check and install dependencies + if not args.quiet: + print_info("Checking dependencies...") + if not ensure_dependencies(auto_install=True, quiet=args.quiet): + print_warning("Some required tools are missing - results may be incomplete") + + # Pick the runner function based on --json flag + extra_kwargs = {} + if args.json: + runner = run_json_diagnostics + # Default JSON extension if user didn't override output + if args.output == 'diagnostic_output.txt': + args.output = 'diagnostic_output.json' + else: + runner = run_diagnostics + + # Determine mode + if args.since: + if args.since == 'boot': + start_time, end_time = get_time_range(1) + else: + start_time, end_time = get_time_range(2) + + exit_code = runner( + start_time, end_time, + output_file=args.output, + **extra_kwargs + ) + else: + # Interactive menu + choice, start_time, end_time, minutes = interactive_menu() + + if choice == 0: + print() + print_info("Cancelled.") + sys.exit(0) + elif start_time and end_time: + exit_code = runner( + start_time, end_time, + output_file=args.output, + **extra_kwargs + ) + else: + print_error("Invalid choice") + sys.exit(1) + + sys.exit(exit_code) + + +if __name__ == '__main__': + main() + diff --git a/fw-log-tool/framework_diagnostic/audio.py b/fw-log-tool/framework_diagnostic/audio.py new file mode 100644 index 0000000..9373e0d --- /dev/null +++ b/fw-log-tool/framework_diagnostic/audio.py @@ -0,0 +1,363 @@ +""" +Audio diagnostics. + +Detects: +- Sound server (PipeWire vs PulseAudio) +- Session manager (WirePlumber vs pipewire-media-session) +- Default sink/source +- Mute status at sound server level and ALSA level +- Volume levels + +All checks use deterministic command output, no interpretation. +""" + +import os +import re +from dataclasses import dataclass, field +from typing import Optional + +from .utils import run_command +from .dependencies import get_distro_id + + +def _run_user_command(cmd: list[str]) -> tuple[int, str, str]: + """Run a command as the real user, not root. + + Audio commands (pactl, amixer) need the user's PulseAudio/PipeWire + session. When running under sudo, we use 'sudo -u $SUDO_USER' to + drop back to the real user, preserving XDG_RUNTIME_DIR so pactl + can find the socket. + + When running under su -c (Debian), SUDO_USER is not set. Fall back + to loginctl to find the graphical session user. + """ + login_user = os.environ.get('SUDO_USER', '') + login_uid = os.environ.get('SUDO_UID', '') + + if login_user and login_user != 'root' and os.geteuid() == 0: + # sudo case — existing path + runtime_dir = f'/run/user/{login_uid}' if login_uid else f'/run/user/1000' + env_cmd = [ + 'sudo', '-u', login_user, + f'XDG_RUNTIME_DIR={runtime_dir}', + ] + cmd + return run_command(env_cmd) + + if not login_user and os.geteuid() == 0: + # su -c case — SUDO_USER not set, find graphical session user via loginctl + user, uid = _find_graphical_session_user() + if user: + runtime_dir = f'/run/user/{uid}' + shell_cmd = f'XDG_RUNTIME_DIR={runtime_dir} ' + ' '.join(cmd) + return run_command(['su', user, '-c', shell_cmd]) + + return run_command(cmd) + + +def _find_graphical_session_user() -> tuple[str, str]: + """Find the user and UID of the graphical session via loginctl. + + Returns (username, uid) or ('', '') if not found. + """ + if hasattr(_find_graphical_session_user, '_cached'): + return _find_graphical_session_user._cached + + result = ('', '') + rc, stdout, _ = run_command(['loginctl', 'list-sessions', '--no-legend']) + if rc == 0: + for line in stdout.strip().split('\n'): + parts = line.split() + if len(parts) < 3: + continue + session_id = parts[0] + rc2, stdout2, _ = run_command([ + 'loginctl', 'show-session', session_id, + '-p', 'Type', '--value' + ]) + if rc2 == 0 and stdout2.strip() in ('wayland', 'x11'): + # Found graphical session — get user and uid + rc3, stdout3, _ = run_command([ + 'loginctl', 'show-session', session_id, + '-p', 'Name', '--value' + ]) + rc4, stdout4, _ = run_command([ + 'loginctl', 'show-session', session_id, + '-p', 'User', '--value' + ]) + if rc3 == 0 and rc4 == 0: + result = (stdout3.strip(), stdout4.strip()) + break + + _find_graphical_session_user._cached = result + return result + + +@dataclass +class AudioDevice: + """An audio sink or source.""" + name: str = "" # internal name, e.g. "alsa_output.pci-0000_c1_00.1..." + description: str = "" # human name, e.g. "Built-in Audio Analog Stereo" + muted: Optional[bool] = None + volume_pct: Optional[int] = None + + +@dataclass +class AudioInfo: + """Complete audio diagnostic results.""" + # Sound server + server_name: str = "" # "PipeWire", "PulseAudio", or "" + server_version: str = "" + pactl_available: bool = False + + # Session manager (PipeWire only) + session_manager: str = "" # "WirePlumber", "pipewire-media-session", "" + + # Default devices + default_sink: Optional[AudioDevice] = None + default_source: Optional[AudioDevice] = None + + # ALSA level + amixer_available: bool = False + alsa_master_muted: Optional[bool] = None + alsa_master_volume: Optional[int] = None + alsa_capture_muted: Optional[bool] = None + + warnings: list[str] = field(default_factory=list) + + +def _parse_volume(text: str) -> Optional[int]: + """Extract first percentage from pactl volume output. + + e.g. "Volume: front-left: 42330 / 65% / -11.27 dB, ..." -> 65 + """ + m = re.search(r'(\d+)%', text) + if m: + return int(m.group(1)) + return None + + +def _get_sink_description(sink_name: str) -> str: + """Get human-readable description for a sink via pactl list sinks.""" + rc, stdout, _ = _run_user_command(['pactl', 'list', 'sinks', 'short']) + if rc != 0: + return "" + # short format: "index\tname\tmodule\tsample_spec\tstate" + # Try the long form for description + rc, stdout, _ = _run_user_command(['pactl', 'list', 'sinks']) + if rc != 0: + return "" + + # Walk through looking for our sink, then grab Description + in_target = False + for line in stdout.split('\n'): + stripped = line.strip() + if stripped.startswith('Name:') and sink_name in stripped: + in_target = True + elif stripped.startswith('Name:'): + in_target = False + elif in_target and stripped.startswith('Description:'): + return stripped.split(':', 1)[1].strip() + return "" + + +def _get_source_description(source_name: str) -> str: + """Get human-readable description for a source via pactl list sources.""" + rc, stdout, _ = _run_user_command(['pactl', 'list', 'sources']) + if rc != 0: + return "" + + in_target = False + for line in stdout.split('\n'): + stripped = line.strip() + if stripped.startswith('Name:') and source_name in stripped: + in_target = True + elif stripped.startswith('Name:'): + in_target = False + elif in_target and stripped.startswith('Description:'): + return stripped.split(':', 1)[1].strip() + return "" + + +def detect_audio() -> AudioInfo: + """Detect audio configuration and status.""" + if get_distro_id() == 'nixos': + return AudioInfo() + + info = AudioInfo() + + # Check pactl available + rc, stdout, _ = _run_user_command(['pactl', 'info']) + if rc != 0: + return info + info.pactl_available = True + + # Parse server info + for line in stdout.split('\n'): + if line.startswith('Server Name:'): + info.server_name = line.split(':', 1)[1].strip() + elif line.startswith('Server Version:') or line.startswith('server.version'): + info.server_version = line.split(':', 1)[1].strip() + + # Normalize server name + if 'pipewire' in info.server_name.lower() or 'PipeWire' in info.server_name: + raw = info.server_name + info.server_name = 'PipeWire' + # PipeWire server name often contains version + ver_match = re.search(r'(\d+\.\d+[\.\d]*)', raw) + if ver_match and not info.server_version: + info.server_version = ver_match.group(1) + elif 'pulse' in info.server_name.lower(): + info.server_name = 'PulseAudio' + + # Session manager (PipeWire only) + if info.server_name == 'PipeWire': + rc, stdout, _ = _run_user_command(['systemctl', '--user', 'is-active', 'wireplumber.service']) + if rc == 0 and stdout.strip() == 'active': + info.session_manager = 'WirePlumber' + else: + rc, stdout, _ = _run_user_command(['systemctl', '--user', 'is-active', + 'pipewire-media-session.service']) + if rc == 0 and stdout.strip() == 'active': + info.session_manager = 'pipewire-media-session' + + # Default sink + rc, stdout, _ = _run_user_command(['pactl', 'get-default-sink']) + if rc == 0 and stdout.strip(): + sink = AudioDevice(name=stdout.strip()) + sink.description = _get_sink_description(sink.name) + + # Sink mute + rc2, out2, _ = _run_user_command(['pactl', 'get-sink-mute', '@DEFAULT_SINK@']) + if rc2 == 0: + sink.muted = 'yes' in out2.lower() + + # Sink volume + rc2, out2, _ = _run_user_command(['pactl', 'get-sink-volume', '@DEFAULT_SINK@']) + if rc2 == 0: + sink.volume_pct = _parse_volume(out2) + + info.default_sink = sink + + # Default source + rc, stdout, _ = _run_user_command(['pactl', 'get-default-source']) + if rc == 0 and stdout.strip(): + source = AudioDevice(name=stdout.strip()) + source.description = _get_source_description(source.name) + + # Source mute + rc2, out2, _ = _run_user_command(['pactl', 'get-source-mute', '@DEFAULT_SOURCE@']) + if rc2 == 0: + source.muted = 'yes' in out2.lower() + + # Source volume + rc2, out2, _ = _run_user_command(['pactl', 'get-source-volume', '@DEFAULT_SOURCE@']) + if rc2 == 0: + source.volume_pct = _parse_volume(out2) + + info.default_source = source + + # ALSA level + rc, stdout, _ = _run_user_command(['amixer', 'get', 'Master']) + if rc == 0: + info.amixer_available = True + # Parse: "Mono: Playback 42330 [65%] [on]" or [off] + if '[off]' in stdout: + info.alsa_master_muted = True + elif '[on]' in stdout: + info.alsa_master_muted = False + vol = _parse_volume(stdout) + if vol is not None: + info.alsa_master_volume = vol + else: + # amixer might exist but Master might not — try 'amixer scontrols' + rc2, _, _ = _run_user_command(['amixer', 'scontrols']) + if rc2 == 0: + info.amixer_available = True + # Master doesn't exist on this card, not an error + + # ALSA capture mute + rc, stdout, _ = _run_user_command(['amixer', 'get', 'Capture']) + if rc == 0: + if '[off]' in stdout: + info.alsa_capture_muted = True + elif '[on]' in stdout: + info.alsa_capture_muted = False + + # Warnings + if info.default_sink and info.default_sink.muted: + info.warnings.append("Default audio output is muted (sound server)") + if info.default_sink and info.default_sink.volume_pct == 0: + info.warnings.append("Default audio output volume is 0%") + if info.alsa_master_muted: + info.warnings.append("ALSA Master is muted (hardware level)") + if info.alsa_master_volume is not None and info.alsa_master_volume == 0: + info.warnings.append("ALSA Master volume is 0%") + if info.default_source and info.default_source.muted: + info.warnings.append("Default microphone is muted") + if info.alsa_capture_muted: + info.warnings.append("ALSA Capture is muted (hardware level)") + + return info + + +def format_audio_report(audio: AudioInfo) -> list[str]: + """Format audio diagnostic results for the report.""" + if not audio.pactl_available: + if get_distro_id() == 'nixos': + return [ + "Audio:", + " PipeWire with WirePlumber (NixOS)", + " Audio detection not supported in NixOS nix-shell environment.", + " Run: nix-shell -p pulseaudio --run \"pactl info\"", + ] + return ["Audio: pactl not available — cannot detect audio configuration"] + + lines = [] + lines.append("Audio:") + + # Server + ver_str = f" v{audio.server_version}" if audio.server_version else "" + sm_str = f" ({audio.session_manager})" if audio.session_manager else "" + lines.append(f" Server: {audio.server_name}{ver_str}{sm_str}") + + # Default output + if audio.default_sink: + sink = audio.default_sink + desc = sink.description or sink.name + mute_str = "" + if sink.muted: + mute_str = " ❌ MUTED" + elif sink.muted is False: + mute_str = "" + vol_str = f" {sink.volume_pct}%" if sink.volume_pct is not None else "" + lines.append(f" Output: {desc}{vol_str}{mute_str}") + + # Default input + if audio.default_source: + source = audio.default_source + desc = source.description or source.name + # Filter out monitor sources (not real microphones) + if '.monitor' not in source.name: + mute_str = " ❌ MUTED" if source.muted else "" + vol_str = f" {source.volume_pct}%" if source.volume_pct is not None else "" + lines.append(f" Input: {desc}{vol_str}{mute_str}") + + # ALSA level + if audio.amixer_available: + alsa_parts = [] + if audio.alsa_master_muted is not None: + if audio.alsa_master_muted: + alsa_parts.append("Master ❌ MUTED") + else: + vol = f" {audio.alsa_master_volume}%" if audio.alsa_master_volume is not None else "" + alsa_parts.append(f"Master{vol}") + if audio.alsa_capture_muted is True: + alsa_parts.append("Capture ❌ MUTED") + if alsa_parts: + lines.append(f" ALSA: {', '.join(alsa_parts)}") + + # Warnings + for w in audio.warnings: + lines.append(f" ⚠️ {w}") + + return lines diff --git a/fw-log-tool/framework_diagnostic/bluetooth.py b/fw-log-tool/framework_diagnostic/bluetooth.py new file mode 100644 index 0000000..39c6b33 --- /dev/null +++ b/fw-log-tool/framework_diagnostic/bluetooth.py @@ -0,0 +1,154 @@ +""" +Bluetooth adapter and device detection. + +Reads adapter status and connected/paired devices from bluetoothctl. +Also checks rfkill for Bluetooth soft/hard block state. +""" + +import re +from dataclasses import dataclass, field + +from .utils import run_command + + +@dataclass +class BluetoothDevice: + """A paired or connected Bluetooth device.""" + address: str # MAC address + name: str = "" + connected: bool = False + + +@dataclass +class BluetoothInfo: + """Bluetooth subsystem status.""" + adapter_present: bool = False + adapter_name: str = "" # e.g. "hci0" + adapter_powered: bool = False + adapter_address: str = "" # MAC address + + # Devices + connected_devices: list[BluetoothDevice] = field(default_factory=list) + paired_device_count: int = 0 + + # Block state (from rfkill, already detected in hardware.py) + soft_blocked: bool = False + hard_blocked: bool = False + + +def detect_bluetooth() -> BluetoothInfo: + """Detect Bluetooth adapter status and connected devices. + + Checks sysfs first for adapter presence (reliable across all distros), + then uses bluetoothctl for device details if available. + """ + info = BluetoothInfo() + + # Check sysfs for adapter — this is the ground truth + rc, stdout, _ = run_command(['bash', '-c', + 'ls /sys/class/bluetooth/ 2>/dev/null']) + if rc == 0 and stdout.strip(): + adapter_name = stdout.strip().split('\n')[0] # e.g. "hci0" + info.adapter_present = True + info.adapter_name = adapter_name + + # Read adapter address from sysfs + rc2, addr, _ = run_command(['bash', '-c', + f'cat /sys/class/bluetooth/{adapter_name}/address 2>/dev/null']) + if rc2 == 0 and addr.strip(): + info.adapter_address = addr.strip() + + # Fallback: D-Bus for address + if not info.adapter_address: + rc2, addr, _ = run_command(['busctl', 'get-property', 'org.bluez', + f'/org/bluez/{adapter_name}', 'org.bluez.Adapter1', 'Address'], + timeout=5) + if rc2 == 0 and '"' in addr: + info.adapter_address = addr.split('"')[1] + + # Check rfkill for block state + rc, stdout, _ = run_command(['bash', '-c', + 'rfkill -J 2>/dev/null || rfkill list bluetooth 2>/dev/null']) + if rc == 0 and stdout.strip(): + lower = stdout.lower() + if 'soft blocked: yes' in lower or '"soft": "blocked"' in lower: + info.soft_blocked = True + if 'hard blocked: yes' in lower or '"hard": "blocked"' in lower: + info.hard_blocked = True + + if not info.adapter_present: + return info + + # Check powered state via D-Bus (BlueZ exposes this reliably) + rc, stdout, _ = run_command(['busctl', 'get-property', 'org.bluez', + f'/org/bluez/{info.adapter_name}', 'org.bluez.Adapter1', 'Powered'], + timeout=5) + if rc == 0 and 'true' in stdout.lower(): + info.adapter_powered = True + + # Fallback: bluetoothctl for powered + if not info.adapter_powered: + rc, stdout, _ = run_command(['bluetoothctl', 'show'], timeout=5) + if rc == 0: + if 'Powered: yes' in stdout: + info.adapter_powered = True + + # Connected devices (bluetoothctl) + rc, stdout, _ = run_command(['bluetoothctl', 'devices', 'Connected'], timeout=5) + if rc == 0: + for line in stdout.strip().split('\n'): + if not line.strip(): + continue + match = re.match(r'Device\s+([0-9A-Fa-f:]{17})\s+(.*)', line.strip()) + if match: + dev = BluetoothDevice( + address=match.group(1), + name=match.group(2).strip(), + connected=True, + ) + info.connected_devices.append(dev) + + # Paired device count + rc, stdout, _ = run_command(['bluetoothctl', 'devices', 'Paired'], timeout=5) + if rc == 0: + count = 0 + for line in stdout.strip().split('\n'): + if line.strip().startswith('Device '): + count += 1 + info.paired_device_count = count + + return info + + +def format_bluetooth_report(info: BluetoothInfo) -> list[str]: + """Format Bluetooth info for the diagnostic report.""" + lines = [] + + lines.append("Bluetooth:") + + if not info.adapter_present: + lines.append(" Adapter: ❌ Not detected") + return lines + + # Adapter status + power_str = "✅ Powered on" if info.adapter_powered else "❌ Powered off" + lines.append(f" Adapter: {info.adapter_address} ({power_str})") + + # Block state + if info.hard_blocked: + lines.append(" ❌ Hardware blocked (rfkill)") + if info.soft_blocked: + lines.append(" ⚠️ Software blocked (rfkill)") + + # Connected devices + if info.connected_devices: + for dev in info.connected_devices: + lines.append(f" Connected: {dev.name} ({dev.address})") + else: + lines.append(" Connected: None") + + # Paired count + if info.paired_device_count > 0: + lines.append(f" Paired devices: {info.paired_device_count}") + + return lines diff --git a/fw-log-tool/framework_diagnostic/dependencies.py b/fw-log-tool/framework_diagnostic/dependencies.py new file mode 100644 index 0000000..27aaeff --- /dev/null +++ b/fw-log-tool/framework_diagnostic/dependencies.py @@ -0,0 +1,436 @@ +""" +Dependency checking and automatic installation of required tools. + +Supports: +- Debian/Ubuntu/Mint/Pop!_OS (apt) +- Fedora (dnf) +- Arch/Manjaro/EndeavourOS (pacman) +- openSUSE (zypper) +- NixOS (guidance only) +- Immutable distros like Bluefin/Bazzite (skip) +""" + +import os +import subprocess +import shutil +import sys +from pathlib import Path +from typing import Optional +from dataclasses import dataclass + +from .output import print_info, print_warning, print_success + + +@dataclass +class DistroPackages: + """Package names for a specific distro family.""" + install_cmd: list[str] # Command prefix, e.g., ['sudo', 'apt-get', 'install', '-y'] + packages: dict[str, str] # tool_name -> package_name mapping + + +# Required tools and their purposes +REQUIRED_TOOLS = { + 'lspci': 'GPU and hardware detection', + 'lsusb': 'USB device detection', + 'lshw': 'Detailed hardware info (GPU driver)', + 'dmidecode': 'System information (RAM, BIOS)', + 'iw': 'WiFi diagnostics', + 'sensors': 'Temperature monitoring', + 'nvme': 'NVMe drive info', + 'fwupdmgr': 'Firmware updates (LVFS)', +} + +# Optional but helpful tools +OPTIONAL_TOOLS = { + 'upower': 'Battery health', + 'smartctl': 'Disk SMART health', + 'pactl': 'Audio configuration', + 'amixer': 'ALSA mixer status', + 'arecord': 'Microphone capture device detection', + 'bluetoothctl': 'Bluetooth diagnostics', + 'nmcli': 'Network connection and VPN detection', +} + +# Package mappings per distro family +DISTRO_PACKAGES = { + 'debian': DistroPackages( + install_cmd=['sudo', 'apt-get', 'install', '-y', '-qq'], + packages={ + 'lspci': 'pciutils', + 'lsusb': 'usbutils', + 'lshw': 'lshw', + 'dmidecode': 'dmidecode', + 'iw': 'iw', + 'sensors': 'lm-sensors', + 'nvme': 'nvme-cli', + 'smartctl': 'smartmontools', + 'upower': 'upower', + 'pactl': 'pulseaudio-utils', + 'amixer': 'alsa-utils', + 'arecord': 'alsa-utils', + 'bc': 'bc', + 'fwupdmgr': 'fwupd', + 'bluetoothctl': 'bluez', + 'nmcli': 'network-manager', + } + ), + 'fedora': DistroPackages( + install_cmd=['sudo', 'dnf', 'install', '-y', '-q'], + packages={ + 'lspci': 'pciutils', + 'lsusb': 'usbutils', + 'lshw': 'lshw', + 'dmidecode': 'dmidecode', + 'iw': 'iw', + 'sensors': 'lm_sensors', + 'nvme': 'nvme-cli', + 'smartctl': 'smartmontools', + 'upower': 'upower', + 'pactl': 'pulseaudio-utils', + 'amixer': 'alsa-utils', + 'arecord': 'alsa-utils', + 'fwupdmgr': 'fwupd', + 'bluetoothctl': 'bluez', + 'nmcli': 'NetworkManager', + } + ), + 'arch': DistroPackages( + install_cmd=['sudo', 'pacman', '-S', '--needed', '--noconfirm'], + packages={ + 'lspci': 'pciutils', + 'lsusb': 'usbutils', + 'lshw': 'lshw', + 'dmidecode': 'dmidecode', + 'iw': 'iw', + 'sensors': 'lm_sensors', + 'nvme': 'nvme-cli', + 'smartctl': 'smartmontools', + 'upower': 'upower', + 'pactl': 'libpulse', + 'amixer': 'alsa-utils', + 'arecord': 'alsa-utils', + 'fwupdmgr': 'fwupd', + 'bluetoothctl': 'bluez-utils', + 'nmcli': 'networkmanager', + } + ), + 'opensuse': DistroPackages( + install_cmd=['sudo', 'zypper', 'install', '-y'], + packages={ + 'lspci': 'pciutils', + 'lsusb': 'usbutils', + 'lshw': 'lshw', + 'dmidecode': 'dmidecode', + 'iw': 'iw', + 'sensors': 'lm_sensors', + 'nvme': 'nvme-cli', + 'smartctl': 'smartmontools', + 'upower': 'upower', + 'pactl': 'pulseaudio-utils', + 'amixer': 'alsa-utils', + 'arecord': 'alsa-utils', + 'fwupdmgr': 'fwupd', + 'bluetoothctl': 'bluez', + 'nmcli': 'NetworkManager', + } + ), +} + +# Map distro IDs to package families +DISTRO_FAMILY_MAP = { + 'ubuntu': 'debian', + 'debian': 'debian', + 'linuxmint': 'debian', + 'pop': 'debian', + 'elementary': 'debian', + 'zorin': 'debian', + 'kali': 'debian', + 'fedora': 'fedora', + 'rhel': 'fedora', + 'centos': 'fedora', + 'rocky': 'fedora', + 'alma': 'fedora', + 'arch': 'arch', + 'manjaro': 'arch', + 'endeavouros': 'arch', + 'garuda': 'arch', + 'cachyos': 'arch', + 'opensuse-tumbleweed': 'opensuse', + 'opensuse-leap': 'opensuse', + 'opensuse': 'opensuse', +} + +# Immutable distros that shouldn't have packages installed +IMMUTABLE_DISTROS = ['bluefin', 'bazzite', 'silverblue', 'kinoite', 'aurora'] + +# Nix attribute names (tool -> nixpkgs attr) +# Covers all external commands the tool calls that aren't in NixOS base +NIX_PACKAGES = { + # Core diagnostic tools (REQUIRED_TOOLS) + 'lspci': 'pciutils', + 'lsusb': 'usbutils', + 'lshw': 'lshw', + 'dmidecode': 'dmidecode', + 'iw': 'iw', + 'sensors': 'lm_sensors', + 'nvme': 'nvme-cli', + 'fwupdmgr': 'fwupd', + # Optional diagnostic tools (OPTIONAL_TOOLS) + 'upower': 'upower', + 'smartctl': 'smartmontools', + 'amixer': 'alsa-utils', + 'arecord': 'alsa-utils', + 'bluetoothctl': 'bluez', + 'nmcli': 'networkmanager', + # System tools not in NixOS minimal base + 'ip': 'iproute2', + 'ping': 'iputils', + 'xrandr': 'xrandr', + 'modetest': 'libdrm', + 'mokutil': 'mokutil', + 'boltctl': 'bolt', + 'fprintd-list': 'fprintd', + 'powerprofilesctl': 'power-profiles-daemon', + 'tlp-stat': 'tlp', + 'tuned-adm': 'tuned', + 'framework_tool': 'framework-tool', +} + + +def get_distro_id() -> Optional[str]: + """Read distro ID from /etc/os-release.""" + os_release = Path('/etc/os-release') + if not os_release.exists(): + return None + + try: + content = os_release.read_text() + for line in content.split('\n'): + if line.startswith('ID='): + return line.split('=')[1].strip('"').lower() + except Exception: + pass + + return None + + +def get_distro_version() -> Optional[str]: + """Read VERSION_ID from /etc/os-release.""" + os_release = Path('/etc/os-release') + if not os_release.exists(): + return None + try: + content = os_release.read_text() + for line in content.split('\n'): + if line.startswith('VERSION_ID='): + return line.split('=')[1].strip('"') + except Exception: + pass + return None + + +def _get_distro_family(distro_id: Optional[str]) -> Optional[str]: + """Resolve distro ID to package family, falling back to ID_LIKE. + + Checks DISTRO_FAMILY_MAP first. If the ID isn't listed, reads + ID_LIKE from /etc/os-release and checks each parent distro. + This catches unlisted derivatives (e.g. CachyOS -> arch). + """ + if distro_id and distro_id in DISTRO_FAMILY_MAP: + return DISTRO_FAMILY_MAP[distro_id] + + # Fall back to ID_LIKE + os_release = Path('/etc/os-release') + if not os_release.exists(): + return None + + try: + content = os_release.read_text() + for line in content.split('\n'): + if line.startswith('ID_LIKE='): + parents = line.split('=')[1].strip('"').lower().split() + for parent in parents: + if parent in DISTRO_FAMILY_MAP: + return DISTRO_FAMILY_MAP[parent] + except Exception: + pass + + return None + + +def check_tool_available(tool: str) -> bool: + """Check if a tool is available in PATH.""" + return shutil.which(tool) is not None + + +def get_missing_tools() -> tuple[list[str], list[str]]: + """ + Check which required and optional tools are missing. + + Returns: + Tuple of (missing_required, missing_optional) + """ + missing_required = [] + missing_optional = [] + + for tool in REQUIRED_TOOLS: + if not check_tool_available(tool): + missing_required.append(tool) + + for tool in OPTIONAL_TOOLS: + if not check_tool_available(tool): + missing_optional.append(tool) + + return missing_required, missing_optional + + +def install_packages(distro_id: str, packages: list[str], quiet: bool = True) -> bool: + """ + Install packages using the appropriate package manager. + + Args: + distro_id: The distro ID from os-release + packages: List of package names to install + quiet: Suppress output + + Returns: + True if installation succeeded + """ + family = _get_distro_family(distro_id) + if not family or family not in DISTRO_PACKAGES: + return False + + pkg_info = DISTRO_PACKAGES[family] + + # If already root (e.g. Debian's su -c), strip 'sudo' from install command + install_cmd = pkg_info.install_cmd + if os.getuid() == 0 and install_cmd and install_cmd[0] == 'sudo': + install_cmd = install_cmd[1:] + + # Map tool names to package names + pkg_names = [] + for pkg in packages: + if pkg in pkg_info.packages: + pkg_names.append(pkg_info.packages[pkg]) + else: + pkg_names.append(pkg) + + if not pkg_names: + return True + + # Special handling for apt - need to update first + if family == 'debian': + update_cmd = ['apt-get', 'update', '-qq'] if os.getuid() == 0 else ['sudo', 'apt-get', 'update', '-qq'] + try: + subprocess.run( + update_cmd, + capture_output=True, + timeout=60 + ) + except Exception: + pass + + # Install packages + cmd = install_cmd + pkg_names + + try: + result = subprocess.run( + cmd, + capture_output=quiet, + timeout=120 + ) + return result.returncode == 0 + except (subprocess.TimeoutExpired, FileNotFoundError): + return False + + +def ensure_dependencies(auto_install: bool = True, quiet: bool = False) -> bool: + """ + Check and optionally install missing dependencies. + + Args: + auto_install: Whether to automatically install missing packages + quiet: Suppress output + + Returns: + True if all required dependencies are available + """ + distro_id = get_distro_id() + + # Check for immutable distros + if distro_id in IMMUTABLE_DISTROS: + if not quiet: + print_info(f"Running on {distro_id} (immutable) - skipping package installation") + missing_req, _ = get_missing_tools() + return len(missing_req) == 0 + + missing_required, missing_optional = get_missing_tools() + + if not missing_required and not missing_optional: + return True + + # NixOS special handling + if distro_id == 'nixos': + # Already inside a nix-shell reexec — don't loop + if os.environ.get('FW_DIAG_NIX_REEXEC'): + if missing_required and not quiet: + print_warning(f"Still missing after nix-shell: {', '.join(missing_required)}") + print_info("Continuing with available tools...") + return len(missing_required) == 0 + + # Auto-reexec inside nix-shell with all dependencies + nix_pkgs = sorted(set(NIX_PACKAGES.values())) + script = os.path.abspath(sys.argv[0]) + args = sys.argv[1:] + inner_cmd = f"python3 {script}" + (f" {' '.join(args)}" if args else "") + + if not quiet: + print_info("NixOS: fetching missing tools via nix-shell...") + + env = os.environ.copy() + env['FW_DIAG_NIX_REEXEC'] = '1' + try: + os.execvpe( + 'nix-shell', + ['nix-shell', '-p'] + nix_pkgs + ['--run', inner_cmd], + env, + ) + except FileNotFoundError: + if not quiet: + print_warning("nix-shell not found — cannot auto-install") + print_info("Install missing tools manually:") + for pkg in nix_pkgs: + print_info(f" pkgs.{pkg}") + return len(missing_required) == 0 + + return len(missing_required) == 0 + + # Other distros - try to install + if auto_install and _get_distro_family(distro_id): + all_missing = missing_required + missing_optional + + if not quiet: + print_info(f"Installing missing tools: {', '.join(all_missing)}") + + success = install_packages(distro_id, all_missing, quiet=True) + + if success: + if not quiet: + print_success("Dependencies installed successfully") + return True + else: + if not quiet: + print_warning("Some packages failed to install - continuing with available tools") + # Re-check what's actually missing now + missing_required, _ = get_missing_tools() + return len(missing_required) == 0 + + # Unknown distro or no auto-install + if missing_required and not quiet: + print_warning(f"Missing required tools: {', '.join(missing_required)}") + print_info("Install these packages manually:") + for tool in missing_required: + print_info(f" {tool}: {REQUIRED_TOOLS[tool]}") + + return len(missing_required) == 0 + diff --git a/fw-log-tool/framework_diagnostic/distro_compat.py b/fw-log-tool/framework_diagnostic/distro_compat.py new file mode 100644 index 0000000..5092d9d --- /dev/null +++ b/fw-log-tool/framework_diagnostic/distro_compat.py @@ -0,0 +1,344 @@ +""" +Framework device Linux distribution compatibility checking. +""" + +from dataclasses import dataclass +from enum import Enum +from pathlib import Path +from typing import Optional + + +class SupportLevel(Enum): + """Distribution support level.""" + OFFICIALLY_SUPPORTED = 'officially_supported' + COMPATIBLE_COMMUNITY_SUPPORTED = 'community_supported' + UNTESTED = 'untested' + OUTDATED_NEEDS_UPDATE = 'outdated_needs_update' + + +@dataclass +class DistroInfo: + """Linux distribution information.""" + id: str # e.g., "fedora", "ubuntu" + version: str # e.g., "43", "24.04" + pretty_name: str # e.g., "Fedora Linux 43" + + +@dataclass +class CompatibilityResult: + """Result of compatibility check.""" + support_level: SupportLevel + model_name: str + distro_info: DistroInfo + recommendation: str = "" + + +# =========================================================================== +# COMPATIBILITY MATRICES +# =========================================================================== +# +# Source: https://frame.work/linux +# +# Format: model_name -> {distro_id: versions_list, ...} + +# Framework Laptop 16 (AMD Ryzen AI 300) — kernel min 6.15 +FRAMEWORK_LAPTOP_16_AI300 = { + 'model': 'Framework Laptop 16 (AMD Ryzen AI 300)', + 'kernel_min': '6.15', + 'kernel_rec': '6.15+', + 'official': {'fedora': ['44+'], 'ubuntu': ['24.04+']}, +} + +# Framework Laptop 12 (13th Gen Intel Core) — kernel min 6.1 +FRAMEWORK_LAPTOP_12 = { + 'model': 'Framework Laptop 12', + 'kernel_min': '6.1', + 'kernel_rec': '6.13+', + 'official': {'fedora': ['44+'], 'ubuntu': ['24.04+']}, +} + +# Framework Desktop (AMD Ryzen AI Max 300) — kernel min 6.11 +FRAMEWORK_DESKTOP = { + 'model': 'Framework Desktop', + 'kernel_min': '6.11', + 'kernel_rec': '6.15+', + 'official': {'fedora': ['44+'], 'ubuntu': ['24.04+']}, +} + +# Framework Laptop 13 (AMD Ryzen AI 300) — kernel min 6.11 +FRAMEWORK_LAPTOP_13_AI300 = { + 'model': 'Framework Laptop 13 (AMD Ryzen AI 300)', + 'kernel_min': '6.11', + 'kernel_rec': '6.15+', + 'official': {'fedora': ['44+'], 'ubuntu': ['24.04+']}, +} + +# Framework Laptop 13 (Intel Core Ultra Series 1) — kernel min 6.8 +FRAMEWORK_LAPTOP_13_INTEL_ULTRA = { + 'model': 'Framework Laptop 13 (Intel Core Ultra)', + 'kernel_min': '6.8', + 'kernel_rec': '6.12+', + 'official': {'fedora': ['44+'], 'ubuntu': ['24.04+']}, +} + +# Framework Laptop 13 (AMD Ryzen 7040) — kernel min 6.6 +FRAMEWORK_LAPTOP_13_AMD_7040 = { + 'model': 'Framework Laptop 13 (AMD Ryzen 7040)', + 'kernel_min': '6.6', + 'kernel_rec': '6.10+', + 'official': {'fedora': ['44+'], 'ubuntu': ['24.04+']}, +} + +# Framework Laptop 16 (AMD Ryzen 7040) — kernel min 6.6 +FRAMEWORK_LAPTOP_16 = { + 'model': 'Framework Laptop 16', + 'kernel_min': '6.6', + 'kernel_rec': '6.10+', + 'official': {'fedora': ['44+'], 'ubuntu': ['24.04+']}, +} + +# Framework Laptop 13 (13th Gen Intel Core) +FRAMEWORK_LAPTOP_13_INTEL_13GEN = { + 'model': 'Framework Laptop 13 (13th Gen Intel)', + 'official': {'fedora': ['44+'], 'ubuntu': ['24.04+']}, +} + +# Framework Laptop 13 (12th Gen Intel Core) +FRAMEWORK_LAPTOP_13_INTEL_12GEN = { + 'model': 'Framework Laptop 13 (12th Gen Intel)', + 'official': {'fedora': ['44+'], 'ubuntu': ['24.04+']}, +} + +# Framework Laptop 13 (11th Gen Intel Core) +FRAMEWORK_LAPTOP_13_INTEL_11GEN = { + 'model': 'Framework Laptop 13 (11th Gen Intel)', + 'official': {'fedora': ['44+'], 'ubuntu': ['24.04+']}, +} + +# --- New models --- + +# Framework Laptop Pro (Intel Core Ultra 3 Series) +FRAMEWORK_LAPTOP_PRO_INTEL_ULTRA3 = { + 'model': 'Framework Laptop Pro (Intel Core Ultra 3 Series)', + 'official': {'fedora': ['44+'], 'ubuntu': ['24.04+']}, +} + +# Framework Laptop Pro (AMD Ryzen AI 300 Series) +FRAMEWORK_LAPTOP_PRO_AMD_AI300 = { + 'model': 'Framework Laptop Pro (AMD Ryzen AI 300 Series)', + 'official': {'fedora': ['44+'], 'ubuntu': ['24.04+']}, +} + +# Framework Laptop 12 (Intel Core Ultra Series 3) +FRAMEWORK_LAPTOP_12_INTEL_ULTRA3 = { + 'model': 'Framework Laptop 12 (Intel Core Ultra Series 3)', + 'official': {'fedora': ['44+'], 'ubuntu': ['24.04+']}, +} + + +def get_distro_info() -> Optional[DistroInfo]: + """Read distribution information from /etc/os-release.""" + os_release = Path('/etc/os-release') + + if not os_release.exists(): + return None + + info = {} + try: + content = os_release.read_text() + for line in content.split('\n'): + if '=' in line: + key, value = line.split('=', 1) + info[key] = value.strip('"') + except Exception: + return None + + return DistroInfo( + id=info.get('ID', 'unknown'), + version=info.get('VERSION_ID', 'unknown'), + pretty_name=info.get('PRETTY_NAME', 'Unknown Linux') + ) + + +def determine_framework_model(product_name: str, model_version: str, cpu_model: str = "") -> dict: + """ + Determine which Framework model compatibility matrix to use. + + Args: + product_name: From dmidecode system-product-name + model_version: From dmidecode system-version + cpu_model: CPU model string for disambiguation + + Returns: + The appropriate compatibility matrix dict + """ + combined = f"{product_name} {model_version}".lower() + + # Framework Laptop Pro (check before generic model matches). + # Real DMI reports "Laptop 13 Pro"; the shorter "Laptop Pro" is also accepted. + if 'laptop pro' in combined or 'laptop 13 pro' in combined: + if 'ai 300' in cpu_model.lower() or 'ryzen ai' in combined: + return FRAMEWORK_LAPTOP_PRO_AMD_AI300 + elif 'ultra' in cpu_model.lower() or 'core ultra' in combined: + return FRAMEWORK_LAPTOP_PRO_INTEL_ULTRA3 + + # Framework Laptop 12 + if 'laptop 12' in combined: + if 'core ultra' in combined or 'ultra' in cpu_model.lower(): + return FRAMEWORK_LAPTOP_12_INTEL_ULTRA3 + return FRAMEWORK_LAPTOP_12 + + # Framework Desktop + if 'desktop' in combined: + return FRAMEWORK_DESKTOP + + # Framework Laptop 16 + if 'laptop 16' in combined: + if 'ai' in combined or 'ai 300' in cpu_model.lower(): + return FRAMEWORK_LAPTOP_16_AI300 + return FRAMEWORK_LAPTOP_16 + + # Framework Laptop 13 + if 'laptop 13' in combined or 'framework' in combined: + # Check CPU for disambiguation + if 'ai 300' in cpu_model.lower() or 'ai' in combined: + return FRAMEWORK_LAPTOP_13_AI300 + elif 'ultra' in cpu_model.lower() or 'core ultra' in combined: + return FRAMEWORK_LAPTOP_13_INTEL_ULTRA + elif '7040' in cpu_model or 'ryzen' in cpu_model.lower(): + return FRAMEWORK_LAPTOP_13_AMD_7040 + elif '13th gen' in combined or '-13' in cpu_model: + return FRAMEWORK_LAPTOP_13_INTEL_13GEN + elif '12th gen' in combined or '-12' in cpu_model: + return FRAMEWORK_LAPTOP_13_INTEL_12GEN + elif '11th gen' in combined or '-11' in cpu_model: + return FRAMEWORK_LAPTOP_13_INTEL_11GEN + elif 'core i' in cpu_model.lower(): + # Generic Intel — guess by CPU generation number if present + return FRAMEWORK_LAPTOP_13_INTEL_13GEN + + # Default to the most recent/common + return FRAMEWORK_LAPTOP_13_INTEL_13GEN + + # Unknown - return generic Laptop 13 + return FRAMEWORK_LAPTOP_13_INTEL_13GEN + + +def check_version_match(supported_versions: list[str], current_version: str) -> bool: + """Check if current version matches any supported version. + + Supports: + '*' — any version (rolling releases) + '24.04+' — 24.04 or newer (compares major.minor numerically) + '43' — exact match + """ + if '*' in supported_versions: + return True + for sv in supported_versions: + if sv.endswith('+'): + # "24.04+" means >= 24.04 + try: + min_parts = [int(x) for x in sv.rstrip('+').split('.')] + cur_parts = [int(x) for x in current_version.split('.')] + if cur_parts >= min_parts: + return True + except (ValueError, AttributeError): + continue + elif sv == current_version: + return True + return False + + +def check_framework_distro_compatibility( + product_name: str, + model_version: str, + cpu_model: str = "" +) -> Optional[CompatibilityResult]: + """ + Check if the current distro is compatible with the Framework device. + + Args: + product_name: From dmidecode system-product-name + model_version: From dmidecode system-version + cpu_model: CPU model for disambiguation + + Returns: + CompatibilityResult or None if not a Framework device + """ + # Check if this is a Framework device + framework_indicators = ['Framework', 'Laptop Pro', 'Laptop 13', 'Laptop 16', 'Laptop 12', 'Desktop'] + if not any(ind in product_name for ind in framework_indicators): + return None + + # Get current distro + distro = get_distro_info() + if distro is None: + return None + + # Get the appropriate compatibility matrix + compat_matrix = determine_framework_model(product_name, model_version, cpu_model) + model_name = compat_matrix['model'] + + # Global distro-support policy: + # Ubuntu/Fedora meeting the official floor -> officially supported + # Ubuntu/Fedora below the floor -> outdated, needs update + # Everything else -> community supported + if distro.id in ('ubuntu', 'fedora'): + versions = compat_matrix.get('official', {}).get(distro.id, []) + # Robust against missing/unparseable VERSION_ID (treat as community). + try: + parseable = bool([int(x) for x in distro.version.split('.')]) + except (ValueError, AttributeError): + parseable = False + + if parseable and check_version_match(versions, distro.version): + return CompatibilityResult( + support_level=SupportLevel.OFFICIALLY_SUPPORTED, + model_name=model_name, + distro_info=distro + ) + elif parseable: + min_version = versions[0].rstrip('+') if versions else "" + return CompatibilityResult( + support_level=SupportLevel.OUTDATED_NEEDS_UPDATE, + model_name=model_name, + distro_info=distro, + recommendation=( + f"Your {distro.id.title()} {distro.version} is older than the " + f"officially supported {distro.id.title()} {min_version}. " + f"Please update to the current release." + ) + ) + # Unparseable version — fall through to community. + + # Every other distro (any id, any version) is community supported. + return CompatibilityResult( + support_level=SupportLevel.COMPATIBLE_COMMUNITY_SUPPORTED, + model_name=model_name, + distro_info=distro + ) + + +def format_compatibility_report(result: CompatibilityResult) -> list[str]: + """Format compatibility result for the diagnostic report.""" + lines = [] + + lines.append("Distribution Compatibility:") + lines.append(f" Device: {result.model_name}") + lines.append(f" Distribution: {result.distro_info.pretty_name}") + + if result.support_level == SupportLevel.OFFICIALLY_SUPPORTED: + lines.append(" Status: ✅ Officially supported and tested") + elif result.support_level == SupportLevel.COMPATIBLE_COMMUNITY_SUPPORTED: + lines.append(" Status: 🔵 Community supported") + elif result.support_level == SupportLevel.OUTDATED_NEEDS_UPDATE: + lines.append(" Status: ⚠️ Update recommended") + if result.recommendation: + lines.append(f" Note: {result.recommendation}") + else: + lines.append(" Status: ⚠️ Untested configuration") + if result.recommendation: + lines.append(f" Note: {result.recommendation}") + + return lines + diff --git a/fw-log-tool/framework_diagnostic/firmware.py b/fw-log-tool/framework_diagnostic/firmware.py new file mode 100644 index 0000000..7398f55 --- /dev/null +++ b/fw-log-tool/framework_diagnostic/firmware.py @@ -0,0 +1,909 @@ +""" +Firmware status detection. + +Collects: +- fwupd device list (all firmware versions + available updates) +- BIOS version (from dmidecode) +- EC (Embedded Controller) firmware version +- Fingerprint reader detection (Goodix - known Framework trouble source) +- Thunderbolt controller firmware +- Secure Boot status +- Kernel command line (boot parameters / workarounds) +""" + +import os +import re +import shutil +from dataclasses import dataclass, field +from pathlib import Path +from typing import Optional + +from .utils import run_command, run_sudo_command + + +# BIOS & Drivers KB page (single source of truth for all models) +BIOS_DRIVERS_URL = 'https://knowledgebase.frame.work/en_us/bios-and-drivers-downloads-rJ3PaCexh' + +# Goodix fingerprint reader USB vendor:product IDs +# Framework ships these across multiple models +GOODIX_USB_IDS = [ + '27c6:5395', # Goodix FingerPrint (FW13 AMD) + '27c6:530c', # Goodix FingerPrint (FW13 Intel) + '27c6:5584', # Goodix FingerPrint (FW16) + '27c6:5503', # Goodix FingerPrint (variant) + '27c6:538d', # Goodix FingerPrint (variant) + '27c6:5301', # Goodix FingerPrint (variant) +] + + +@dataclass +class FwupdDevice: + """A device reported by fwupd.""" + name: str + device_id: str = "" + current_version: str = "" + update_available: bool = False + update_version: str = "" + vendor: str = "" + flags: list[str] = field(default_factory=list) + guid: str = "" + + + + +@dataclass +class FingerprintInfo: + """Fingerprint reader detection and fprintd status.""" + detected: bool = False + model: str = "" + usb_id: str = "" + driver_loaded: bool = False + driver_name: str = "" + # fprintd details + fprintd_installed: bool = False + fprintd_version: str = "" + fprintd_running: bool = False + fprintd_enabled: bool = False + enrolled_fingers: list[str] = field(default_factory=list) # e.g. ["right-index-finger"] + enrolled_user: str = "" # user we checked enrollment for + pam_configured: bool = False # fingerprint auth in PAM + warnings: list[str] = field(default_factory=list) + + +@dataclass +class FirmwareInfo: + """Complete firmware status.""" + # fwupd devices + fwupd_available: bool = False + fwupd_daemon_available: bool = True # False if fwupdmgr can't reach daemon + fwupd_service_enabled: bool = True # False if systemd service not enabled (NixOS D-Bus auto-start hides this) + fwupd_devices: list[FwupdDevice] = field(default_factory=list) + updates_available: int = 0 + lvfs_refresh_failed: bool = False + + # BIOS + bios_version: str = "" + + # EC + ec_version: str = "" + + # Fingerprint + fingerprint: FingerprintInfo = field(default_factory=FingerprintInfo) + + # Boot config + kernel_cmdline: str = "" + secure_boot: Optional[bool] = None # None = unknown, True/False = detected + + # Thunderbolt + thunderbolt_fw_version: str = "" + + # Framework System Tool + framework_tool_versions: str = "" # output of framework_tool --versions + framework_tool_pd_info: str = "" # output of framework_tool --pd-info + + +def _find_fwupdtool() -> Optional[str]: + """Find fwupdtool binary. + + Checks PATH first, then looks relative to fwupdmgr (common on NixOS + where the binary lives under /nix/store/.../libexec/fwupd/fwupdtool). + + Returns path string or None. + """ + # PATH lookup works on all distros including NixOS nix-shell + found = shutil.which('fwupdtool') + if found: + return found + + # fwupdtool often lives at ../libexec/fwupd/fwupdtool relative to fwupdmgr + fwupdmgr = shutil.which('fwupdmgr') + if fwupdmgr: + mgr_dir = Path(fwupdmgr).resolve().parent + candidate = mgr_dir.parent / 'libexec' / 'fwupd' / 'fwupdtool' + if candidate.is_file(): + return str(candidate) + + return None + + +def _run_fwupd_command(args: list[str], timeout: int = 15) -> tuple[int, str, str]: + """Run an fwupd command, falling back to fwupdtool if daemon unavailable. + + Tries fwupdmgr first. If it fails (e.g. daemon not running on NixOS), + falls back to fwupdtool which works standalone without the D-Bus daemon. + + Note: fwupdtool does NOT support --no-unreported-check (fwupdmgr-only, + for LVFS reporting prompts). This is stripped automatically when falling back. + fwupdtool DOES support --force (per man page). + + Returns (returncode, stdout, stderr). + """ + rc, stdout, stderr = run_command(args, timeout=timeout) + if rc == 0: + return rc, stdout, stderr + + # Check if failure is due to daemon unavailability + daemon_errors = ['could not connect', 'failed to connect', 'no such file or directory', + 'org.freedesktop.fwupd', 'service is not running', 'timed out', + 'command not found'] + combined = (stdout + stderr).lower() + is_daemon_issue = any(err in combined for err in daemon_errors) + + if not is_daemon_issue: + return rc, stdout, stderr + + # Try fwupdtool fallback + tool_path = _find_fwupdtool() + if not tool_path: + return rc, stdout, stderr + + # Build fwupdtool command: replace fwupdmgr with fwupdtool, + # strip flags it doesn't support (--no-unreported-check is fwupdmgr-only) + unsupported_flags = {'--no-unreported-check'} + new_args = [tool_path] + [a for a in args[1:] if a not in unsupported_flags] + + return run_command(new_args, timeout=timeout) + + +def get_fwupd_devices() -> tuple[bool, list[FwupdDevice]]: + """ + Get device firmware status from fwupd. + + Uses 'fwupdmgr get-devices --json' for structured output, + falls back to text parsing. + + Returns: + Tuple of (fwupd_available, device_list) + """ + devices = [] + + # Try JSON output first (fwupd >= 1.7.0) + rc, stdout, _ = _run_fwupd_command(['fwupdmgr', 'get-devices', '--json', '--no-unreported-check'], timeout=15) + if rc == 0 and stdout.strip(): + try: + import json + data = json.loads(stdout) + # fwupd JSON has {"Devices": [...]} + for dev in data.get('Devices', []): + device = FwupdDevice( + name=dev.get('Name', 'Unknown'), + device_id=dev.get('DeviceId', ''), + current_version=dev.get('Version', ''), + vendor=dev.get('Vendor', ''), + guid=dev.get('Guid', [''])[0] if dev.get('Guid') else '', + ) + # Check flags for updatable + flags = dev.get('Flags', []) + device.flags = flags if isinstance(flags, list) else [] + + devices.append(device) + return True, devices + except (ImportError, ValueError, KeyError): + pass + + # Fallback: text output + rc, stdout, _ = _run_fwupd_command(['fwupdmgr', 'get-devices', '--no-unreported-check'], timeout=15) + if rc == 0 and stdout.strip(): + current_device = None + for line in stdout.split('\n'): + # Device header lines are indented with the device name + # Followed by properties like "Device ID: ..." + stripped = line.strip() + + if not stripped: + if current_device: + devices.append(current_device) + current_device = None + continue + + if not line.startswith(' ') and not line.startswith('\t') and ':' not in stripped: + # This is likely a device name line + if current_device: + devices.append(current_device) + current_device = FwupdDevice(name=stripped) + elif current_device and ':' in stripped: + key, _, value = stripped.partition(':') + key = key.strip() + value = value.strip() + if key == 'Device ID': + current_device.device_id = value + elif key == 'Current version': + current_device.current_version = value + elif key == 'Vendor': + current_device.vendor = value + elif key == 'Update Version': + current_device.update_available = True + current_device.update_version = value + + if current_device: + devices.append(current_device) + + return True, devices + + # fwupd not available + return False, [] + + +def check_fwupd_updates() -> tuple[int, bool]: + """ + Check how many firmware updates are available. + + Refreshes LVFS metadata first to ensure results are current. + On NixOS, uses fwupdtool directly (daemon may auto-start via D-Bus + but won't have LVFS remotes configured without services.fwupd.enable). + + Returns: + Tuple of (count of available updates, whether refresh failed) + """ + from .dependencies import get_distro_id + + # NixOS without services.fwupd.enable: use fwupdtool directly — + # daemon auto-starts via D-Bus but has no LVFS remotes configured. + # When service IS enabled ("linked"), use normal fwupdmgr path below. + if get_distro_id() == 'nixos': + rc_svc, stdout_svc, _ = run_command(['systemctl', 'is-enabled', 'fwupd.service'], timeout=5) + nixos_service_enabled = (rc_svc == 0 or stdout_svc.strip() in ('linked', 'linked-runtime')) + if not nixos_service_enabled: + tool_path = _find_fwupdtool() + if tool_path: + rc, _, _ = run_command([tool_path, 'refresh', '--force'], timeout=30) + refresh_failed = rc != 0 + + rc, stdout, _ = run_command([tool_path, 'get-updates', '--json'], timeout=15) + if rc == 0 and stdout.strip(): + try: + import json + data = json.loads(stdout) + return len(data.get('Devices', [])), refresh_failed + except (ImportError, ValueError): + pass + + rc, stdout, _ = run_command([tool_path, 'get-updates'], timeout=15) + if rc == 0: + count = 0 + for line in stdout.split('\n'): + if 'Update Version' in line or 'New version' in line: + count += 1 + return count, refresh_failed + + return 0, refresh_failed + + # All other distros: use fwupdmgr (with fwupdtool fallback if daemon unavailable) + # Refresh metadata from LVFS first — stale/missing metadata means no results + # --force ensures metadata is actually downloaded, not skipped due to cache age + rc, _, _ = _run_fwupd_command(['fwupdmgr', 'refresh', '--force', '--no-unreported-check'], timeout=30) + refresh_failed = rc != 0 + + rc, stdout, _ = _run_fwupd_command(['fwupdmgr', 'get-updates', '--json', '--no-unreported-check'], timeout=15) + if rc == 0 and stdout.strip(): + try: + import json + data = json.loads(stdout) + return len(data.get('Devices', [])), refresh_failed + except (ImportError, ValueError): + pass + + # Fallback: text output, count device sections + rc, stdout, _ = _run_fwupd_command(['fwupdmgr', 'get-updates', '--no-unreported-check'], timeout=15) + if rc == 0: + # Count lines that look like device update headers + count = 0 + for line in stdout.split('\n'): + if 'Update Version' in line or 'New version' in line: + count += 1 + return count, refresh_failed + + return 0, refresh_failed + + + + +def detect_fingerprint_reader() -> FingerprintInfo: + """ + Detect Goodix fingerprint reader and fprintd status. + + Framework laptops use Goodix readers (vendor ID 27c6) which are a + known source of suspend issues and driver problems. + + Collects: + - Hardware presence (lsusb) + - Kernel driver (lsmod) + - fprintd service status and version + - Enrolled fingers (fprintd-list) + - PAM configuration + - Known issue warnings + """ + info = FingerprintInfo() + + rc, stdout, _ = run_command(['lsusb']) + if rc != 0: + return info + + for line in stdout.split('\n'): + match = re.search(r'ID\s+([0-9a-f]{4}:[0-9a-f]{4})', line, re.IGNORECASE) + if match: + usb_id = match.group(1).lower() + if usb_id in GOODIX_USB_IDS or usb_id.startswith('27c6:'): + info.detected = True + info.usb_id = usb_id + # Extract name from lsusb line + name_match = re.search(r'ID\s+[0-9a-f:]+\s+(.+)$', line) + if name_match: + info.model = name_match.group(1).strip() + else: + info.model = 'Goodix Fingerprint Reader' + break + + # Check kernel driver + if info.detected: + rc, stdout, _ = run_command(['lsmod']) + if rc == 0: + for mod in ['goodix_ts', 'goodix', 'usbhid']: + if mod in stdout: + info.driver_loaded = True + info.driver_name = mod + break + + # fprintd service status + rc, stdout, _ = run_command(['systemctl', 'is-active', 'fprintd.service']) + if rc == 0 and stdout.strip() == 'active': + info.fprintd_running = True + if info.detected: + info.driver_loaded = True + if not info.driver_name: + info.driver_name = 'fprintd/libfprint' + + rc, stdout, _ = run_command(['systemctl', 'is-enabled', 'fprintd.service']) + if rc == 0 and stdout.strip() in ('enabled', 'static'): + info.fprintd_enabled = True + info.fprintd_installed = True + + # fprintd version + rc, stdout, _ = run_command(['fprintd-list', '--version']) + if rc != 0: + # Try package manager queries + rc, stdout, _ = run_command(['bash', '-c', + 'rpm -q fprintd 2>/dev/null || ' + 'dpkg -l fprintd 2>/dev/null | grep ^ii | awk \'{print $3}\' || ' + 'pacman -Q fprintd 2>/dev/null']) + if rc == 0 and stdout.strip(): + ver_match = re.search(r'(\d+\.\d+[\.\d]*)', stdout) + if ver_match: + info.fprintd_version = ver_match.group(1) + info.fprintd_installed = True + + # Enrolled fingers - try current $SUDO_USER or $USER + login_user = os.environ.get('SUDO_USER', os.environ.get('USER', '')) + if login_user and login_user != 'root': + info.enrolled_user = login_user + rc, stdout, _ = run_command(['fprintd-list', login_user]) + if rc == 0: + info.fprintd_installed = True + # Output format: + # "Using device /dev/XX" + # "Fingerprints for user matt on ... :" + # " - #0: right-index-finger" + for line in stdout.split('\n'): + line = line.strip() + if line.startswith('- #') or line.startswith('-#'): + # Extract finger name + finger_match = re.search(r':\s*(.+)$', line) + if finger_match: + info.enrolled_fingers.append(finger_match.group(1).strip()) + + # PAM configuration - check if pam_fprintd.so is active + pam_paths = [ + '/etc/pam.d/system-auth', # Fedora/RHEL + '/etc/pam.d/common-auth', # Debian/Ubuntu + '/etc/pam.d/login', # Generic + '/etc/pam.d/sudo', # Sudo fingerprint + '/etc/pam.d/gdm-fingerprint', # GNOME + '/etc/pam.d/sddm', # KDE + ] + for pam_path in pam_paths: + try: + pam_content = Path(pam_path).read_text() + for pam_line in pam_content.split('\n'): + stripped = pam_line.strip() + # Active (not commented out) pam_fprintd.so line + if 'pam_fprintd.so' in stripped and not stripped.startswith('#'): + info.pam_configured = True + break + except (OSError, PermissionError): + pass + if info.pam_configured: + break + + # Warnings for known issues + if info.detected: + if not info.fprintd_installed: + info.warnings.append("fprintd not installed — fingerprint auth unavailable") + elif not info.fprintd_running and info.fprintd_enabled: + # fprintd is socket-activated, so not running is normal until first use + pass + + if info.fprintd_installed and not info.enrolled_fingers and info.enrolled_user: + info.warnings.append(f"No fingers enrolled for {info.enrolled_user}") + + if info.fprintd_installed and not info.pam_configured: + info.warnings.append("pam_fprintd.so not found in PAM config — " + "fingerprint login/sudo won't work") + + return info + + +def get_kernel_cmdline() -> str: + """Read kernel boot command line from /proc/cmdline.""" + cmdline_path = Path('/proc/cmdline') + try: + if cmdline_path.exists(): + return cmdline_path.read_text().strip() + except PermissionError: + pass + return "" + + +def get_secure_boot_status() -> Optional[bool]: + """ + Check Secure Boot status. + + Returns: + True = enabled, False = disabled, None = unknown + """ + # Method 1: mokutil + rc, stdout, _ = run_command(['mokutil', '--sb-state']) + if rc == 0: + if 'SecureBoot enabled' in stdout: + return True + elif 'SecureBoot disabled' in stdout: + return False + + # Method 2: Check EFI variable directly + sb_path = Path('/sys/firmware/efi/efivars/SecureBoot-8be4df61-93ca-11d2-aa0d-00e098032b8c') + if sb_path.exists(): + try: + data = sb_path.read_bytes() + # Last byte: 1 = enabled, 0 = disabled + if len(data) >= 5: + return data[4] == 1 + except PermissionError: + pass + + # Method 3: bootctl (systemd-boot) + rc, stdout, _ = run_command(['bootctl', 'status']) + if rc == 0: + for line in stdout.split('\n'): + if 'Secure Boot' in line: + if 'enabled' in line.lower(): + return True + elif 'disabled' in line.lower(): + return False + + return None + + +def get_ec_version() -> str: + """ + Get EC (Embedded Controller) firmware version. + + Tries multiple methods: + 1. ectool (Framework's tool) + 2. fwupd device list (EC shows up as updatable device) + 3. dmidecode + """ + # Method 1: ectool + rc, stdout, _ = run_command(['ectool', 'version']) + if rc == 0: + for line in stdout.split('\n'): + if 'RO version' in line or 'RW version' in line: + parts = line.split(':') + if len(parts) >= 2: + return parts[1].strip() + + # Method 2: dmidecode BIOS information (sometimes has EC version) + rc, stdout, _ = run_sudo_command(['dmidecode', '-t', 'bios']) + if rc == 0: + for line in stdout.split('\n'): + if 'EC' in line and 'Version' in line: + parts = line.split(':') + if len(parts) >= 2: + return parts[1].strip() + # Framework-specific: "Firmware Revision" in BIOS info + if 'Firmware Revision' in line: + parts = line.split(':') + if len(parts) >= 2: + version = parts[1].strip() + if version and version != '0.0': + return version + + return "" + + +def get_thunderbolt_fw_version() -> str: + """Get Thunderbolt controller firmware version if present.""" + # Try boltctl + rc, stdout, _ = run_command(['boltctl', 'list']) + if rc == 0: + for line in stdout.split('\n'): + if 'nvm-version' in line.lower() or 'firmware' in line.lower(): + parts = line.split(':') + if len(parts) >= 2: + return parts[1].strip() + + return "" + + +_FRAMEWORK_TOOL_URL = ( + 'https://github.com/FrameworkComputer/framework-system' + '/releases/latest/download/framework_tool' +) +_FRAMEWORK_TOOL_PATH = Path('/tmp/framework_tool') + + +def _download_framework_tool() -> bool: + """Download framework_tool binary from GitHub. + + Returns True if download succeeded and binary is executable. + """ + import socket + import urllib.request + import urllib.error + + old_timeout = socket.getdefaulttimeout() + try: + socket.setdefaulttimeout(15) + urllib.request.urlretrieve( + _FRAMEWORK_TOOL_URL, str(_FRAMEWORK_TOOL_PATH), + ) + _FRAMEWORK_TOOL_PATH.chmod(0o755) + return _FRAMEWORK_TOOL_PATH.is_file() + except (urllib.error.URLError, OSError, socket.timeout): + return False + finally: + socket.setdefaulttimeout(old_timeout) + + +def _run_framework_tool() -> tuple[str, str]: + """Download and run framework_tool --versions and --pd-info. + + Returns (versions_output, pd_info_output). Empty strings on failure. + """ + # Check PATH first (works on all distros including NixOS nix-shell) + tool_path = shutil.which('framework_tool') + + # Fallback: check common hardcoded locations + if not tool_path: + for candidate in ['/usr/bin/framework_tool', '/usr/local/bin/framework_tool']: + if Path(candidate).is_file(): + tool_path = candidate + break + + # Download if not installed + if not tool_path: + if _download_framework_tool(): + tool_path = str(_FRAMEWORK_TOOL_PATH) + else: + return "", "" + + versions = "" + pd_info = "" + + rc, stdout, _ = run_sudo_command([tool_path, '--versions'], timeout=10) + if rc == 0 and stdout.strip(): + versions = stdout.strip() + + rc, stdout, _ = run_sudo_command([tool_path, '--pd-info'], timeout=10) + if rc == 0 and stdout.strip(): + pd_info = stdout.strip() + + # Clean up downloaded binary + if tool_path == str(_FRAMEWORK_TOOL_PATH): + try: + _FRAMEWORK_TOOL_PATH.unlink() + except OSError: + pass + + return versions, pd_info + + +def detect_firmware_info( + bios_version: str = "", + is_framework: bool = False +) -> FirmwareInfo: + """ + Detect all firmware-related information. + + Args: + bios_version: Current BIOS version from hardware detection + is_framework: Whether this is a Framework device + + Returns: + FirmwareInfo with all collected data + """ + info = FirmwareInfo() + + # Test if fwupd daemon is reachable (required for update checks) + rc, _, stderr = run_command(['fwupdmgr', 'get-devices', '--json', '--no-unreported-check'], timeout=10) + daemon_errors = ['could not connect', 'failed to connect', 'org.freedesktop.fwupd', + 'service is not running', 'timed out', 'command not found'] + if rc != 0 and any(err in (stderr or '').lower() for err in daemon_errors): + info.fwupd_daemon_available = False + + # Check if fwupd service is actually enabled (not just D-Bus auto-started) + # On NixOS without services.fwupd.enable, the daemon auto-starts via D-Bus + # but has no LVFS remotes configured — so it can list devices but can't check updates + # NixOS with services.fwupd.enable reports "linked" (exit 1), not "enabled" (exit 0) + rc_enabled, stdout_enabled, _ = run_command(['systemctl', 'is-enabled', 'fwupd.service'], timeout=5) + if rc_enabled != 0 and stdout_enabled.strip() not in ('linked', 'linked-runtime'): + info.fwupd_service_enabled = False + + # fwupd devices (uses fwupdtool fallback if daemon unavailable) + info.fwupd_available, info.fwupd_devices = get_fwupd_devices() + + # Check for updates (uses fwupdtool fallback if daemon unavailable) + if info.fwupd_available: + info.updates_available, info.lvfs_refresh_failed = check_fwupd_updates() + + # BIOS version (from dmidecode, passed in by caller) + info.bios_version = bios_version + + # EC version (Framework only) + if is_framework: + info.ec_version = get_ec_version() + + # Fingerprint reader + info.fingerprint = detect_fingerprint_reader() + + # Boot config + info.kernel_cmdline = get_kernel_cmdline() + info.secure_boot = get_secure_boot_status() + + # Thunderbolt + info.thunderbolt_fw_version = get_thunderbolt_fw_version() + + # Framework System Tool (Framework only, needs internet) + if is_framework: + info.framework_tool_versions, info.framework_tool_pd_info = _run_framework_tool() + + return info + + +def format_firmware_report(info: FirmwareInfo) -> list[str]: + """Format firmware info for the diagnostic report.""" + lines = [] + + lines.append("Firmware Status:") + + # BIOS version + if info.bios_version: + lines.append(f" BIOS: {info.bios_version}") + lines.append(f" BIOS & Drivers: {BIOS_DRIVERS_URL}") + + # EC version + if info.ec_version: + lines.append(f" EC Firmware: {info.ec_version}") + + # Secure Boot + if info.secure_boot is True: + lines.append(" Secure Boot: Enabled") + elif info.secure_boot is False: + lines.append(" Secure Boot: Disabled") + + # Thunderbolt firmware + if info.thunderbolt_fw_version: + lines.append(f" Thunderbolt FW: {info.thunderbolt_fw_version}") + + # Kernel command line + if info.kernel_cmdline: + lines.append(f" Kernel cmdline: {info.kernel_cmdline}") + # Surface non-standard parameters + interesting_params = _extract_interesting_boot_params(info.kernel_cmdline) + if interesting_params: + lines.append(" Non-default kernel parameters:") + for param, note in interesting_params: + if note: + lines.append(f" {param} — {note}") + else: + lines.append(f" {param}") + + # Fingerprint reader + if info.fingerprint.detected: + fp = info.fingerprint + driver_str = f", driver: {fp.driver_name}" if fp.driver_loaded else ", no driver" + lines.append(f" Fingerprint: {fp.model} ({fp.usb_id}{driver_str})") + + # fprintd status + if fp.fprintd_installed: + ver_str = f" v{fp.fprintd_version}" if fp.fprintd_version else "" + svc_icon = "✅" if fp.fprintd_running or fp.fprintd_enabled else "❌" + # fprintd is socket-activated, so "inactive" is normal + if fp.fprintd_enabled and not fp.fprintd_running: + svc_status = "enabled (socket-activated)" + svc_icon = "✅" + elif fp.fprintd_running: + svc_status = "running" + else: + svc_status = "not enabled" + lines.append(f" fprintd{ver_str}: {svc_icon} {svc_status}") + + # Enrollment + if fp.enrolled_fingers: + fingers = ', '.join(fp.enrolled_fingers) + lines.append(f" Enrolled ({fp.enrolled_user}): {fingers}") + elif fp.enrolled_user: + lines.append(f" Enrolled ({fp.enrolled_user}): ❌ No fingers enrolled") + + # PAM + pam_icon = "✅" if fp.pam_configured else "❌" + pam_str = "configured" if fp.pam_configured else "not configured" + lines.append(f" PAM auth: {pam_icon} {pam_str}") + else: + lines.append(" fprintd: ❌ Not installed") + + # Warnings + for warning in fp.warnings: + lines.append(f" ⚠️ {warning}") + else: + lines.append(" Fingerprint: Not detected") + + # fwupd devices + if info.fwupd_available: + lines.append(f" fwupd: {len(info.fwupd_devices)} device(s) managed") + if not info.fwupd_daemon_available: + lines.append(" ⚠️ fwupd daemon not running") + # Distro-specific fix + from .dependencies import get_distro_id + distro_id = get_distro_id() + if distro_id == 'nixos': + lines.append(" Fix: add 'services.fwupd.enable = true;' to configuration.nix") + lines.append(" Then run: sudo nixos-rebuild switch") + else: + lines.append(" Fix: sudo systemctl enable --now fwupd") + elif not info.fwupd_service_enabled: + # NixOS: daemon auto-started via D-Bus but service not enabled — no LVFS remotes + from .dependencies import get_distro_id + if get_distro_id() == 'nixos': + lines.append(" ⚠️ fwupd service not enabled — cannot check LVFS for firmware updates") + lines.append(" Fix: add 'services.fwupd.enable = true;' to configuration.nix") + lines.append(" Then run: sudo nixos-rebuild switch") + if info.updates_available > 0: + lines.append(f" ⚠️ {info.updates_available} firmware update(s) available") + lines.append(" Run: fwupdmgr get-updates && fwupdmgr update") + elif info.lvfs_refresh_failed: + lines.append(" ⚠️ Could not check LVFS for firmware updates (metadata refresh failed)") + from .dependencies import get_distro_id + if get_distro_id() == 'nixos': + lines.append(" Fix: add 'services.fwupd.enable = true;' to configuration.nix") + lines.append(" Then run: sudo nixos-rebuild switch") + + # List devices with versions (only those with versions, skip empty) + versioned = [d for d in info.fwupd_devices if d.current_version] + if versioned: + lines.append(" fwupd device versions:") + for dev in versioned: + update_str = f" → {dev.update_version}" if dev.update_available else "" + lines.append(f" {dev.name}: {dev.current_version}{update_str}") + else: + from .dependencies import get_distro_id + if get_distro_id() == 'nixos': + lines.append(" fwupd: ⚠️ Not available") + lines.append(" Fix: add 'services.fwupd.enable = true;' to configuration.nix") + lines.append(" Then run: sudo nixos-rebuild switch") + else: + lines.append(" fwupd: Not available (install fwupd for firmware management)") + + # Framework System Tool + if info.framework_tool_versions: + lines.append("") + lines.append(" Framework System Tool (framework_tool --versions):") + for line in info.framework_tool_versions.split('\n'): + if line.strip(): + lines.append(f" {line.strip()}") + + if info.framework_tool_pd_info: + lines.append("") + lines.append(" Framework System Tool (framework_tool --pd-info):") + for line in info.framework_tool_pd_info.split('\n'): + if line.strip(): + lines.append(f" {line.strip()}") + + return lines + + +def _extract_interesting_boot_params(cmdline: str) -> list[tuple[str, str]]: + """ + Surface non-standard kernel parameters by stripping known boot + infrastructure. Everything left is either user-added or a distro + hardware workaround — either way, support needs to see it. + + Returns list of (parameter, note) tuples for display. + Note is empty string for unknown params, filled in for recognized ones. + """ + # ── Standard boot infrastructure (never interesting) ───────── + # These are set by bootloaders, initramfs, and default distro configs + # across Fedora, Ubuntu, Arch, openSUSE, etc. + _STANDARD_EXACT = frozenset({ + 'ro', 'rw', 'quiet', 'splash', 'rhgb', 'noresume', 'noplymouth', + }) + + _STANDARD_PREFIXES = ( + 'BOOT_IMAGE=', 'root=', 'rootflags=', 'rootfstype=', 'initrd=', + 'resume=', + 'rd.', # dracut/initramfs (rd.luks, rd.lvm, rd.md, etc.) + 'loglevel=', 'audit=', 'crashkernel=', + 'systemd.', # systemd boot params + 'plymouth.', 'vt.handoff=', + 'lang=', 'console=', + 'apparmor=', 'security=', + ) + + # ── Known workarounds (annotate when recognized) ───────────── + _KNOWN_NOTES = { + 'amdgpu.runpm=0': 'AMD GPU runtime PM disabled (power/suspend workaround)', + 'amdgpu.dcdebugmask=0x10': 'PSR disabled via AMD debug mask', + 'amdgpu.ppfeaturemask=': 'AMD GPU power features overridden', + 'i915.enable_psr=0': 'Intel Panel Self Refresh disabled', + 'i915.enable_dc=0': 'Intel display C-states disabled', + 'nvme_core.default_ps_max_latency_us=': 'NVMe power state latency override', + 'mem_sleep_default=deep': 'Forced S3 deep sleep (not s2idle)', + 'mem_sleep_default=s2idle': 'Forced s2idle sleep mode', + 'acpi_osi=': 'ACPI OS identification override', + 'acpi=off': 'ACPI completely disabled (unusual)', + 'nomodeset': 'Kernel modesetting disabled (GPU issues)', + 'iommu=pt': 'IOMMU passthrough mode', + 'amd_iommu=off': 'AMD IOMMU disabled', + 'intel_iommu=on': 'Intel IOMMU enabled', + 'snd_hda_intel.power_save=': 'HDA audio power save setting', + 'iwlwifi.power_save=0': 'Intel WiFi power saving disabled', + 'usbcore.autosuspend=-1': 'USB autosuspend disabled globally', + 'ec_intr=0': 'EC interrupt mode disabled (workaround)', + 'tpm_tis.interrupts=0': 'TPM interrupts disabled (boot speed workaround)', + 'pcie_aspm=off': 'PCIe Active State Power Management disabled', + 'pcie_aspm.policy=': 'PCIe ASPM policy override', + 'mitigations=off': 'CPU vulnerability mitigations disabled', + } + + interesting = [] + for param in cmdline.split(): + # Skip standard boot infrastructure + if param in _STANDARD_EXACT: + continue + if any(param.startswith(p) for p in _STANDARD_PREFIXES): + continue + + # Look up annotation for known params + note = '' + for key, desc in _KNOWN_NOTES.items(): + if key.endswith('='): + if param.startswith(key): + note = desc + break + else: + if param == key: + note = desc + break + + interesting.append((param, note)) + + return interesting + diff --git a/fw-log-tool/framework_diagnostic/fw12.py b/fw-log-tool/framework_diagnostic/fw12.py new file mode 100644 index 0000000..b5900cb --- /dev/null +++ b/fw-log-tool/framework_diagnostic/fw12.py @@ -0,0 +1,740 @@ +""" +Framework Laptop 12 specific diagnostics. + +The FW12 is a 2-in-1 convertible with features not present on other models: +- Tablet mode (lid folds 360°, disables keyboard/trackpad) +- Screen rotation (accelerometer-based via cros_ec) +- Touchscreen + stylus support + +These features require specific kernel modules and services. +Reference: Framework 12 Debugging Guide +""" + +from dataclasses import dataclass +from pathlib import Path +from typing import Optional +import re +from .utils import run_command +from .dependencies import get_distro_id, _get_distro_family, get_distro_version +import re + + +@dataclass +class TabletModeStatus: + """Tablet mode hardware/driver status.""" + # Kernel modules + pinctrl_loaded: bool = False + pinctrl_builtin: bool = False # =y in kernel config (not a loadable module) + soc_button_loaded: bool = False + # GPIO detection + gpio_keys_detected: bool = False + # Overall + working: bool = False + issue: str = "" + + +@dataclass +class ScreenRotationStatus: + """Screen rotation (accelerometer) status.""" + # EC driver + cros_ec_detected: bool = False + # Sensor driver + cros_ec_sensors_loaded: bool = False + # IIO device present + iio_accel_present: bool = False + # iio-sensor-proxy service + sensor_proxy_running: bool = False + sensor_proxy_version: str = "" + # iio-buffer-accel udev rule (3.7 bug: active = broken) + iio_buffer_accel_rule_active: Optional[bool] = None # None = couldn't check + # Functional test: does SensorProxy actually report a working accelerometer? + accel_functional: Optional[bool] = None # None = couldn't check + # Overall + working: bool = False + issue: str = "" + + +@dataclass +class FW12Diagnostics: + """All Framework 12 specific diagnostic results.""" + is_fw12: bool = False + tablet_mode: Optional[TabletModeStatus] = None + screen_rotation: Optional[ScreenRotationStatus] = None + is_kde: bool = False + plasma_major: int = 0 # 5 or 6 + plasma_minor: int = 0 # e.g. 6 for 6.6 + virtual_keyboard_installed: bool = False + virtual_keyboard_pkg: str = "" # which package was found + + +def check_tablet_mode() -> TabletModeStatus: + """Check tablet mode functionality. + + Requires: + 1. pinctrl_tigerlake kernel module loaded (must load before soc_button_array) + 2. soc_button_array kernel module loaded + 3. gpio-keys input device present in /proc/bus/input/devices + """ + status = TabletModeStatus() + + # Check kernel modules + rc, stdout, _ = run_command(['lsmod']) + if rc == 0: + status.pinctrl_loaded = 'pinctrl_tigerlake' in stdout + status.soc_button_loaded = 'soc_button_array' in stdout + + # Fallback: pinctrl_tigerlake may be built-in (=y) rather than a loadable module (=m). + # Debian kernels do this. lsmod won't show built-in modules. + if not status.pinctrl_loaded: + rc, stdout, _ = run_command(['bash', '-c', + 'cat /boot/config-$(uname -r) 2>/dev/null | grep CONFIG_PINCTRL_TIGERLAKE']) + if rc == 0 and 'CONFIG_PINCTRL_TIGERLAKE=y' in stdout: + status.pinctrl_loaded = True + status.pinctrl_builtin = True + + # Check gpio-keys input device actually exists (not dmesg — ring buffer is unreliable) + rc, stdout, _ = run_command(['bash', '-c', + 'cat /proc/bus/input/devices 2>/dev/null']) + if rc == 0 and 'gpio-keys' in stdout.lower(): + status.gpio_keys_detected = True + else: + # Fallback: check sysfs input device names + rc, stdout, _ = run_command(['bash', '-c', + 'cat /sys/class/input/*/name 2>/dev/null']) + if rc == 0: + status.gpio_keys_detected = 'gpio-keys' in stdout.lower() + + # Determine status + if status.pinctrl_loaded and status.soc_button_loaded and status.gpio_keys_detected: + status.working = True + elif not status.pinctrl_loaded and not status.soc_button_loaded: + status.issue = "Kernel modules not loaded: pinctrl_tigerlake, soc_button_array" + elif not status.pinctrl_loaded: + status.issue = "pinctrl_tigerlake not loaded (must load before soc_button_array)" + elif not status.soc_button_loaded: + status.issue = "soc_button_array not loaded" + elif not status.gpio_keys_detected: + status.issue = ("gpio-keys not detected — pinctrl_tigerlake may have loaded " + "after soc_button_array.") + + return status + + +def check_screen_rotation() -> ScreenRotationStatus: + """Check screen rotation (accelerometer) functionality. + + Requires: + 1. cros_ec driver recognized the system (kernel 6.12+) + 2. cros_ec_sensors module for accelerometer data + 3. IIO accelerometer device exposed in sysfs + 4. iio-sensor-proxy daemon running + + Known issue: iio-sensor-proxy 3.7 has a regression that breaks + accelerometer. Fixed in 3.8. Workaround: downgrade to 3.6 or + edit udev rules. + """ + status = ScreenRotationStatus() + + # Check cros_ec device exists in sysfs (not dmesg — ring buffer is unreliable) + rc, stdout, _ = run_command(['bash', '-c', + 'ls /sys/bus/platform/devices/cros_ec* 2>/dev/null || ' + 'ls /sys/class/chromeos/cros_ec 2>/dev/null']) + if rc == 0 and stdout.strip(): + status.cros_ec_detected = True + else: + # Fallback: check if cros_ec module is loaded + rc, stdout, _ = run_command(['lsmod']) + if rc == 0 and 'cros_ec' in stdout: + status.cros_ec_detected = True + + # Check cros_ec_sensors module + rc, stdout, _ = run_command(['lsmod']) + if rc == 0: + status.cros_ec_sensors_loaded = 'cros_ec_sensors' in stdout + + # Check IIO accelerometer device + rc, stdout, _ = run_command(['bash', '-c', + 'for d in /sys/bus/iio/devices/iio:device*/name; do ' + 'cat "$d" 2>/dev/null; done']) + if rc == 0: + status.iio_accel_present = 'cros-ec-accel' in stdout + + # Check iio-sensor-proxy service + rc, stdout, _ = run_command(['systemctl', 'is-active', 'iio-sensor-proxy.service']) + if rc == 0: + status.sensor_proxy_running = stdout.strip() == 'active' + + # Get iio-sensor-proxy version (important: 3.7 is broken) + rc, stdout, _ = run_command(['bash', '-c', + 'iio-sensor-proxy --version 2>/dev/null || ' + 'rpm -q iio-sensor-proxy 2>/dev/null || ' + 'dpkg -l iio-sensor-proxy 2>/dev/null | grep ^ii | awk \'{print $3}\' || ' + 'pacman -Q iio-sensor-proxy 2>/dev/null']) + if rc == 0 and stdout.strip(): + # Extract version number + ver_match = re.search(r'(\d+\.\d+)', stdout) + if ver_match: + status.sensor_proxy_version = ver_match.group(1) + + # NixOS fallback: check the binary in the nix store for version info + if not status.sensor_proxy_version: + rc, stdout, _ = run_command(['bash', '-c', + 'readlink -f $(which iio-sensor-proxy 2>/dev/null) 2>/dev/null || ' + 'find /nix/store -maxdepth 2 -name iio-sensor-proxy -type f 2>/dev/null | head -1']) + if rc == 0 and '/nix/store/' in (stdout or ''): + # Extract version from nix store path (e.g. /nix/store/xxx-iio-sensor-proxy-3.7/...) + ver_match = re.search(r'iio-sensor-proxy-(\d+\.\d+)', stdout) + if ver_match: + status.sensor_proxy_version = ver_match.group(1) + + # Check if the iio-buffer-accel udev rule is active (the actual 3.7 bug trigger). + # If this rule is uncommented, iio-sensor-proxy grabs the accel as a buffer device + # and desktop environments can't get orientation events. + # Check override first (/etc), then system rule locations. + rc, stdout, _ = run_command(['bash', '-c', + 'grep -l "iio-buffer-accel" ' + '/etc/udev/rules.d/80-iio-sensor-proxy.rules ' + '/usr/lib/udev/rules.d/80-iio-sensor-proxy.rules ' + '/nix/store/*/lib/udev/rules.d/80-iio-sensor-proxy.rules ' + '2>/dev/null | head -1']) + if rc == 0 and stdout.strip(): + rules_file = stdout.strip() + rc2, content, _ = run_command(['bash', '-c', f'grep "iio-buffer-accel" "{rules_file}"']) + if rc2 == 0 and content.strip(): + # Check if the line is commented out (fix applied) or active (bug present) + active_lines = [l for l in content.strip().split('\n') + if 'iio-buffer-accel' in l and not l.strip().startswith('#')] + status.iio_buffer_accel_rule_active = len(active_lines) > 0 + + # Functional test: ask SensorProxy if accelerometer actually works. + # This catches cases where 3.7 + active udev rule is NOT broken (e.g. Debian Trixie). + if status.sensor_proxy_running: + rc, stdout, _ = run_command(['busctl', 'get-property', + 'net.hadess.SensorProxy', '/net/hadess/SensorProxy', + 'net.hadess.SensorProxy', 'HasAccelerometer']) + if rc == 0 and stdout.strip(): + status.accel_functional = 'true' in stdout.lower() + + # Determine status + # If the IIO accel device exists in sysfs, cros_ec worked regardless + # of whether we found the dmesg message (ring buffer may have rolled) + if not status.iio_accel_present and not status.cros_ec_detected: + status.issue = "cros_ec not detected — kernel 6.12+ required for FW12 accelerometer" + elif not status.iio_accel_present: + status.issue = "cros_ec detected but IIO accelerometer device not found — cros_ec_sensors module may not be loaded" + elif not status.sensor_proxy_running: + status.issue = "iio-sensor-proxy service not running" + elif status.sensor_proxy_version == '3.7' and status.iio_buffer_accel_rule_active is True: + if status.accel_functional is True: + # 3.7 + active rule but accelerometer works — not actually broken + status.working = True + else: + status.issue = ("iio-sensor-proxy 3.7 iio-buffer-accel udev rule is active — " + "this breaks accelerometer on Framework 12") + # Hardware side works, just the udev rule is wrong + status.working = True + elif status.sensor_proxy_version == '3.7' and status.iio_buffer_accel_rule_active is False: + # 3.7 with patched rule — bug is fixed, no warning needed + status.working = True + elif status.sensor_proxy_version == '3.7': + # 3.7 but couldn't determine rule status — warn to be safe + status.issue = ("iio-sensor-proxy 3.7 detected — check that iio-buffer-accel " + "udev rule is patched (commented out)") + status.working = True + else: + status.working = True + + return status + + +def detect_fw12_diagnostics(model_type: str, desktop_environment: str = "") -> FW12Diagnostics: + """Run Framework 12 specific diagnostics. + + Only runs on FW12 hardware. + """ + diag = FW12Diagnostics() + + if model_type != 'Laptop 12': + return diag + + diag.is_fw12 = True + diag.tablet_mode = check_tablet_mode() + diag.screen_rotation = check_screen_rotation() + + # KDE virtual keyboard check + if 'kde' in desktop_environment.lower() or 'plasma' in desktop_environment.lower(): + diag.is_kde = True + + # Detect Plasma version + rc, stdout, _ = run_command(['plasmashell', '--version'], timeout=5) + if rc == 0: + # Output: "plasmashell 6.6.0" or "plasmashell 5.27.11" + m = re.search(r'(\d+)\.(\d+)', stdout) + if m: + diag.plasma_major = int(m.group(1)) + diag.plasma_minor = int(m.group(2)) + + if diag.plasma_major >= 6: + # Plasma 6: plasma-keyboard (6.6+) or maliit-keyboard (6.0-6.5) + # Check both — distro packaging varies + for pkg in ('plasma-keyboard', 'maliit-keyboard'): + rc, _, _ = run_command(['pacman', '-Q', pkg]) + if rc == 0: + diag.virtual_keyboard_installed = True + diag.virtual_keyboard_pkg = pkg + break + rc, _, _ = run_command(['bash', '-c', + f'dpkg -l {pkg} 2>/dev/null | grep -q ^ii || ' + f'rpm -q {pkg} 2>/dev/null']) + if rc == 0: + diag.virtual_keyboard_installed = True + diag.virtual_keyboard_pkg = pkg + break + else: + # Plasma 5: maliit-keyboard (Qt5) + rc, _, _ = run_command(['which', 'maliit-keyboard']) + if rc == 0: + diag.virtual_keyboard_installed = True + diag.virtual_keyboard_pkg = 'maliit-keyboard' + else: + rc, _, _ = run_command(['bash', '-c', + 'dpkg -l maliit-keyboard 2>/dev/null | grep -q ^ii || ' + 'rpm -q maliit-keyboard 2>/dev/null']) + if rc == 0: + diag.virtual_keyboard_installed = True + diag.virtual_keyboard_pkg = 'maliit-keyboard' + + return diag + + +def format_fw12_report(diag: FW12Diagnostics) -> list[str]: + """Format FW12 diagnostic results for the report.""" + if not diag.is_fw12: + return [] + + distro_id = get_distro_id() + distro_family = _get_distro_family(distro_id) + distro_version = get_distro_version() + + # Linux Mint (based on Ubuntu 24.04) and Ubuntu 24.x — no FW12 tablet support + if distro_id == 'linuxmint' or (distro_id == 'ubuntu' and distro_version and distro_version.startswith('24.')): + return ["Framework 12 Features:", + " Framework 12 tablet mode requires Ubuntu 25.10 or later."] + + lines = [] + lines.append("Framework 12 Features:") + lines.append("") + + # Tablet mode status + tm = diag.tablet_mode + if tm: + if tm.working: + lines.append(" Tablet Mode: ✅ Working") + else: + lines.append(" Tablet Mode: ❌ Not working") + if tm.issue: + lines.append(f" Issue: {tm.issue}") + pinctrl_label = '✅ (built-in)' if tm.pinctrl_builtin else ('✅' if tm.pinctrl_loaded else '❌') + lines.append(f" pinctrl_tigerlake: {pinctrl_label}") + lines.append(f" soc_button_array: {'✅' if tm.soc_button_loaded else '❌'}") + lines.append(f" gpio-keys: {'✅' if tm.gpio_keys_detected else '❌'}") + # Debian: show persistent fix status when working + # If pinctrl is built-in, soc_button_array loads reliably every boot — no conf needed + if distro_id == 'debian' and tm.working and not tm.pinctrl_builtin: + if Path('/etc/modules-load.d/fw12-tablet.conf').exists(): + lines.append(" fw12-tablet.conf: ✅ installed") + else: + lines.append(" ⚠️ Working now, but may not persist across reboots") + lines.append(" Permanent fix (run as root with: su -):") + if tm.pinctrl_builtin: + lines.append(' echo "soc_button_array" > /etc/modules-load.d/fw12-tablet.conf') + else: + lines.append(' echo -e "pinctrl_tigerlake\\nsoc_button_array" > /etc/modules-load.d/fw12-tablet.conf') + + lines.append("") + + # Screen rotation status + sr = diag.screen_rotation + if sr: + if sr.working and not sr.issue: + lines.append(" Screen Rotation: ✅ Working") + elif sr.working and sr.issue: + lines.append(" Screen Rotation: ⚠️ Working (with known issue)") + else: + lines.append(" Screen Rotation: ❌ Not working") + if sr.issue: + lines.append(f" Issue: {sr.issue}") + cros_ec_ok = sr.cros_ec_detected or sr.iio_accel_present + ver_str = f" v{sr.sensor_proxy_version}" if sr.sensor_proxy_version else "" + lines.append(f" cros_ec: {'✅' if cros_ec_ok else '❌'}") + lines.append(f" iio-accel: {'✅' if sr.iio_accel_present else '❌'}") + lines.append(f" sensor-proxy: {'✅' if sr.sensor_proxy_running else '❌'}{ver_str}") + if sr.iio_buffer_accel_rule_active is True and sr.sensor_proxy_version == '3.7': + if sr.accel_functional is True: + lines.append(" udev rule: ✅ active (accelerometer functional)") + else: + lines.append(" udev rule: ⚠️ iio-buffer-accel active (causes 3.7 regression)") + elif sr.iio_buffer_accel_rule_active is False and sr.sensor_proxy_version == '3.7': + lines.append(" udev rule: ✅ patched") + + # NixOS: full step-by-step guide when fixes are needed + if distro_id == 'nixos': + has_fixes = (tm and not tm.working) or (sr and (not sr.working or sr.issue)) + if has_fixes: + lines.extend(_nixos_fw12_guide(diag)) + return lines + + # Non-NixOS: use distro-specific fix suggestions + if tm and not tm.working: + distro_version = get_distro_version() if distro_id == 'ubuntu' else None + lines.extend(_tablet_mode_fix(tm, distro_id, distro_family, distro_version)) + + if sr and (not sr.working or sr.issue): + lines.extend(_rotation_fix(sr, distro_id, distro_family)) + + # Virtual keyboard (KDE only, non-NixOS) + if diag.is_kde: + if diag.virtual_keyboard_installed: + lines.append(f" On-Screen Keyboard: ✅ {diag.virtual_keyboard_pkg} installed") + else: + lines.append(f" On-Screen Keyboard: ❌ Not installed") + lines.extend(_virtual_keyboard_fix(distro_id, distro_family, + diag.plasma_major, diag.plasma_minor)) + + return lines + + +def _nixos_fw12_guide(diag: FW12Diagnostics) -> list[str]: + """Full step-by-step NixOS fix guide for Framework 12.""" + lines = [] + lines.append("") + lines.append("Framework 12 NixOS Fix: Tablet Mode + Screen Rotation") + lines.append("======================================================") + lines.append("") + lines.append("STEP 1: Add the nixos-hardware channel") + lines.append("---------------------------------------") + lines.append("Copy and paste this entire line into your terminal:") + lines.append("") + lines.append("sudo nix-channel --add https://github.com/NixOS/nixos-hardware/archive/master.tar.gz nixos-hardware && sudo nix-channel --update") + lines.append("") + lines.append("Wait for it to finish.") + lines.append("") + lines.append("") + lines.append("STEP 2: Edit configuration.nix") + lines.append("-------------------------------") + lines.append("Open the file:") + lines.append("") + lines.append("sudo nano /etc/nixos/configuration.nix") + lines.append("") + lines.append("Find the imports section near the top. It looks something like this:") + lines.append("") + lines.append(" imports = [") + lines.append(" ./hardware-configuration.nix") + lines.append(" ];") + lines.append("") + lines.append("Change it to:") + lines.append("") + lines.append(" imports = [") + lines.append(" ./hardware-configuration.nix") + lines.append(" ") + lines.append(" ];") + lines.append("") + lines.append("Then find an empty line anywhere in the file and add:") + lines.append("") + lines.append(" hardware.sensor.iio.enable = true;") + lines.append("") + lines.append("Save and exit: Ctrl+O, Enter, Ctrl+X") + lines.append("") + lines.append("") + lines.append("STEP 3: Rebuild and reboot") + lines.append("---------------------------") + lines.append("Copy and paste:") + lines.append("") + lines.append("sudo nixos-rebuild switch") + lines.append("") + lines.append("") + lines.append("STEP 4: After reboot, run the diagnostic again") + lines.append("-----------------------------------------------") + lines.append("Both Tablet Mode and Screen Rotation should show as working.") + lines.append("") + lines.append("GNOME will provide rotation now, but, KDE Plasma has proven to be more") + lines.append("reliable on NixOS for onscreen keyboard.") + lines.append("") + lines.append("To switch to KDE Plasma, add the following to configuration.nix:") + lines.append("") + lines.append(" # Enable the KDE Plasma Desktop Environment.") + lines.append(" services.displayManager.sddm.enable = true;") + lines.append(" services.desktopManager.plasma6.enable = true;") + lines.append("") + lines.append("") + lines.append(" environment.systemPackages = with pkgs; [") + lines.append(" # ... your other packages ...") + lines.append(" maliit-keyboard") + lines.append(" maliit-framework") + lines.append(" ];") + lines.append("") + lines.append("") + lines.append(" services.displayManager.sddm.settings = {") + lines.append(" General = {") + lines.append(' InputMethod = "qtvirtualkeyboard";') + lines.append(" };") + lines.append(" };") + lines.append(" services.displayManager.sddm.extraPackages = [ pkgs.kdePackages.qtvirtualkeyboard ];") + lines.append("") + lines.append("") + lines.append(" sudo nixos-rebuild switch") + + return lines + + +def _tablet_mode_fix(tm: TabletModeStatus, distro_id: Optional[str], + distro_family: Optional[str], + distro_version: Optional[str] = None) -> list[str]: + """Generate distro-specific tablet mode fix suggestions.""" + lines = [] + + if distro_id == 'nixos': + lines.append("") + lines.append(" Tablet Mode Fix (recommended):") + lines.append(" Add to configuration.nix:") + lines.append(" imports = [ ];") + lines.append("") + lines.append(" Tablet Mode Fix (manual alternative):") + lines.append(" Add to configuration.nix:") + lines.append(" boot.initrd.kernelModules = [ \"pinctrl_tigerlake\" ];") + elif distro_family == 'arch': + if not tm.pinctrl_loaded: + lines.append("") + lines.append(" Tablet Mode Fix:") + lines.append(" sudo modprobe pinctrl_tigerlake") + lines.append(" sudo modprobe soc_button_array") + elif tm.pinctrl_loaded and tm.soc_button_loaded and not tm.gpio_keys_detected: + # Boot race — pinctrl loaded after soc_button_array + lines.append("") + lines.append(" Tablet Mode Fix (temporary):") + lines.append(" sudo rmmod soc_button_array && sudo modprobe soc_button_array") + + # Persistent fix for boot race + if not Path('/etc/systemd/system/fw12-tablet-fix.service').exists(): + lines.append("") + lines.append(" Tablet Mode Fix (permanent):") + lines.append(' printf \'[Unit]\\nDescription=Reload soc_button_array for FW12 tablet mode\\nAfter=multi-user.target\\n\\n[Service]\\nType=oneshot\\nExecStart=/bin/sh -c "rmmod soc_button_array && modprobe soc_button_array"\\n\\n[Install]\\nWantedBy=multi-user.target\\n\' | sudo tee /etc/systemd/system/fw12-tablet-fix.service') + lines.append(" sudo systemctl enable fw12-tablet-fix.service") + else: + lines.append(" fw12-tablet-fix.service: ✅ installed") + elif distro_id == 'ubuntu': + if distro_version and (distro_version.startswith('24.') or distro_version == '25.04'): + lines.append("") + lines.append(" Framework 12 tablet mode requires Ubuntu 25.10 or later.") + elif distro_version and distro_version.startswith('25.10'): + svc_exists = Path('/etc/systemd/system/reload-soc-module.service').exists() + if svc_exists: + lines.append(" reload-soc-module.service: ✅ installed") + else: + lines.append("") + lines.append(" Tablet Mode Fix (Ubuntu 25.10 workaround):") + lines.append(" See: https://github.com/FrameworkComputer/linux-docs/blob/main/framework12/Ubuntu-25-04-accel-ubuntu25.04.md#2504-and-2510-both-apply-to-this-guide") + else: + # Other Ubuntu versions — generic fix + if not tm.pinctrl_loaded: + lines.append("") + lines.append(" Tablet Mode Fix:") + lines.append(" sudo modprobe pinctrl_tigerlake") + lines.append(" sudo modprobe soc_button_array") + elif not tm.gpio_keys_detected: + lines.append("") + lines.append(" Tablet Mode Fix (module load order race):") + lines.append(" sudo rmmod soc_button_array && sudo modprobe soc_button_array") + elif distro_id == 'debian': + conf_exists = Path('/etc/modules-load.d/fw12-tablet.conf').exists() + if not tm.pinctrl_loaded: + lines.append("") + lines.append(" Tablet Mode Fix (run as root with: su -):") + lines.append(" modprobe pinctrl_tigerlake") + lines.append(" modprobe soc_button_array") + elif not tm.soc_button_loaded: + lines.append("") + lines.append(" Tablet Mode Fix (run as root with: su -):") + lines.append(" modprobe soc_button_array") + elif not tm.gpio_keys_detected: + lines.append("") + lines.append(" Tablet Mode Fix (run as root with: su -):") + lines.append(" rmmod soc_button_array && modprobe soc_button_array") + if not conf_exists: + lines.append("") + lines.append(" Tablet Mode Fix (permanent, run as root with: su -):") + if tm.pinctrl_builtin: + lines.append(' echo "soc_button_array" > /etc/modules-load.d/fw12-tablet.conf') + else: + lines.append(' echo -e "pinctrl_tigerlake\\nsoc_button_array" > /etc/modules-load.d/fw12-tablet.conf') + else: + lines.append(" fw12-tablet.conf: ✅ installed") + else: + # Generic Linux + if not tm.pinctrl_loaded: + lines.append("") + lines.append(" Tablet Mode Fix:") + lines.append(" sudo modprobe pinctrl_tigerlake") + lines.append(" sudo modprobe soc_button_array") + elif not tm.gpio_keys_detected: + lines.append("") + lines.append(" Tablet Mode Fix (module load order race):") + lines.append(" sudo rmmod soc_button_array && sudo modprobe soc_button_array") + + return lines + + +def _rotation_fix(sr: ScreenRotationStatus, distro_id: Optional[str], + distro_family: Optional[str]) -> list[str]: + """Generate distro-specific screen rotation fix suggestions.""" + lines = [] + + if distro_id == 'nixos': + # Only show fixes relevant to the actual problem + if not sr.cros_ec_detected or not sr.iio_accel_present: + lines.append("") + lines.append(" Screen Rotation Fix (hardware module):") + lines.append(" Add to configuration.nix:") + lines.append(" imports = [ ];") + + if not sr.sensor_proxy_running: + lines.append("") + lines.append(" Screen Rotation Fix:") + lines.append(" Add to configuration.nix:") + lines.append(" hardware.sensor.iio.enable = true;") + + if sr.iio_buffer_accel_rule_active is True and sr.sensor_proxy_version == '3.7': + lines.append("") + lines.append(" Screen Rotation Fix (iio-sensor-proxy 3.7 bug):") + lines.append(" iio-sensor-proxy 3.7 has a broken udev rule (iio-buffer-accel).") + lines.append(" NixOS 25.05 and 25.11 (unstable) include the fix — update your system.") + lines.append("") + lines.append(" If you can't update, add this overlay to configuration.nix:") + lines.append(" nixpkgs.overlays = [") + lines.append(" (final: prev: {") + lines.append(" iio-sensor-proxy = prev.iio-sensor-proxy.overrideAttrs (oldAttrs: {") + lines.append(" postPatch = oldAttrs.postPatch + ''") + lines.append(" sed -i -e 's/.*iio-buffer-accel/#&/' data/80-iio-sensor-proxy.rules") + lines.append(" '';") + lines.append(" });") + lines.append(" })") + lines.append(" ];") + elif sr.iio_buffer_accel_rule_active is None and sr.sensor_proxy_version == '3.7': + lines.append("") + lines.append(" Note: iio-sensor-proxy 3.7 detected but couldn't verify udev rule status.") + lines.append(" NixOS 25.05 and 25.11 include the fix. If you're on an older release,") + lines.append(" see: https://github.com/FrameworkComputer/linux-docs/blob/main/framework12/nixOS.md") + elif distro_family == 'arch': + if not sr.sensor_proxy_running: + lines.append("") + lines.append(" Screen Rotation Fix:") + lines.append(" sudo pacman -S iio-sensor-proxy") + lines.append(" sudo systemctl enable --now iio-sensor-proxy") + if sr.iio_buffer_accel_rule_active is True and sr.sensor_proxy_version == '3.7': + lines.append("") + lines.append(" Screen Rotation Fix (iio-sensor-proxy 3.7 bug):") + lines.append(" sudo sed 's/.*iio-buffer-accel/#&/' /usr/lib/udev/rules.d/80-iio-sensor-proxy.rules | sudo tee /etc/udev/rules.d/80-iio-sensor-proxy.rules") + lines.append(" sudo udevadm trigger --settle") + lines.append(" sudo systemctl restart iio-sensor-proxy") + elif sr.iio_buffer_accel_rule_active is None and sr.sensor_proxy_version == '3.7': + lines.append("") + lines.append(" Screen Rotation Note (iio-sensor-proxy 3.7):") + lines.append(" Check if iio-buffer-accel udev rule needs patching:") + lines.append(" grep iio-buffer-accel /usr/lib/udev/rules.d/80-iio-sensor-proxy.rules") + lines.append(" If the line is NOT commented out, patch it:") + lines.append(" sudo sed 's/.*iio-buffer-accel/#&/' /usr/lib/udev/rules.d/80-iio-sensor-proxy.rules | sudo tee /etc/udev/rules.d/80-iio-sensor-proxy.rules") + lines.append(" sudo udevadm trigger --settle") + lines.append(" sudo systemctl restart iio-sensor-proxy") + elif distro_id == 'ubuntu': + if not sr.sensor_proxy_running: + lines.append("") + lines.append(" Screen Rotation Fix:") + lines.append(" sudo apt install iio-sensor-proxy") + lines.append(" sudo systemctl enable --now iio-sensor-proxy") + if sr.iio_buffer_accel_rule_active is True and sr.sensor_proxy_version == '3.7': + lines.append("") + lines.append(" Screen Rotation Fix (iio-sensor-proxy 3.7 bug):") + lines.append(" See: https://github.com/FrameworkComputer/linux-docs/blob/main/framework12/Ubuntu-25-04-accel-ubuntu25.04.md#2504-and-2510-both-apply-to-this-guide") + elif distro_id == 'debian': + if not sr.sensor_proxy_running: + lines.append("") + lines.append(" Screen Rotation Fix (run as root with: su -):") + lines.append(" apt install iio-sensor-proxy") + lines.append(" systemctl enable --now iio-sensor-proxy") + if sr.iio_buffer_accel_rule_active is True and sr.sensor_proxy_version == '3.7': + lines.append("") + lines.append(" Screen Rotation Fix (iio-sensor-proxy 3.7 bug, run as root with: su -):") + lines.append(" sed 's/.*iio-buffer-accel/#&/' /usr/lib/udev/rules.d/80-iio-sensor-proxy.rules > /etc/udev/rules.d/80-iio-sensor-proxy.rules") + lines.append(" udevadm trigger --settle") + lines.append(" systemctl restart iio-sensor-proxy") + else: + # Generic + if not sr.sensor_proxy_running: + lines.append("") + lines.append(" Screen Rotation Fix:") + lines.append(" Install and enable iio-sensor-proxy for your distribution.") + + return lines + + +def _virtual_keyboard_fix(distro_id: Optional[str], + distro_family: Optional[str], + plasma_major: int = 6, + plasma_minor: int = 0) -> list[str]: + """Generate distro-specific on-screen keyboard install instructions for KDE Plasma.""" + lines = [] + + # plasma-keyboard is new in Plasma 6.6; before that, all distros used maliit-keyboard. + # Debian doesn't package plasma-keyboard regardless of version. + has_plasma_kbd = (plasma_major > 6 or (plasma_major == 6 and plasma_minor >= 6)) \ + and distro_family != 'debian' + + if has_plasma_kbd: + pkg = "plasma-keyboard" + settings_path = "System Settings → Keyboard → Virtual Keyboard" + else: + pkg = "maliit-keyboard" + if plasma_major >= 6: + settings_path = "System Settings → Keyboard → Virtual Keyboard → select Maliit" + else: + settings_path = "System Settings → Input & Output → Virtual Keyboard → select Maliit" + + if distro_id == 'nixos': + lines.append("") + lines.append(f" Install {pkg} for on-screen keyboard in tablet mode.") + lines.append(f" Search for the package: https://search.nixos.org/packages?query={pkg}") + elif distro_family == 'arch': + if has_plasma_kbd: + # plasma-keyboard is in [extra] + lines.append("") + lines.append(" To install:") + lines.append(f" sudo pacman -S {pkg}") + else: + # maliit-keyboard is AUR-only on Arch + lines.append("") + lines.append(f" {pkg} is in the AUR (not in official repos).") + lines.append(" To install with an AUR helper:") + lines.append(f" paru -S {pkg}") + lines.append(" or:") + lines.append(f" yay -S {pkg}") + elif distro_family == 'fedora': + lines.append("") + lines.append(" To install:") + lines.append(f" sudo dnf install {pkg}") + elif distro_family == 'debian': + lines.append("") + lines.append(" To install:") + lines.append(f" sudo apt install {pkg}") + elif distro_family == 'opensuse': + lines.append("") + lines.append(" To install:") + lines.append(f" sudo zypper install {pkg}") + else: + lines.append("") + lines.append(f" Install {pkg} for your distribution.") + + lines.append("") + lines.append(" To activate in KDE Plasma:") + lines.append(f" {settings_path}") + + return lines diff --git a/fw-log-tool/framework_diagnostic/hardware.py b/fw-log-tool/framework_diagnostic/hardware.py new file mode 100644 index 0000000..9ae7acb --- /dev/null +++ b/fw-log-tool/framework_diagnostic/hardware.py @@ -0,0 +1,1771 @@ +""" +Hardware detection for GPU, NVMe, WiFi, RAM, and Framework-specific components. +""" + +import os +import re +from dataclasses import dataclass, field +from pathlib import Path +from typing import Optional +from enum import Enum + +from .utils import run_command, run_sudo_command + + +class CPUVendor(Enum): + """CPU vendor types.""" + AMD = 'AMD' + INTEL = 'Intel' + UNKNOWN = 'Unknown' + + +class AMDGeneration(Enum): + """AMD processor generation for thermal threshold calibration.""" + MODERN = 'modern' # Ryzen 7000/AI 300 series - runs hot by design + LEGACY = 'legacy' # Older Ryzen - lower thermal limits + + +@dataclass +class GPUInfo: + """GPU hardware information.""" + name: str + pci_id: str + vendor: str # AMD, Intel, NVIDIA + driver: str = "" + is_discrete: bool = False + + +@dataclass +class NVMeInfo: + """NVMe storage device information.""" + device: str # e.g., /dev/nvme0n1 + model: str + pci_id: str = "" + firmware: str = "" + + +@dataclass +class DiskHealthInfo: + """Disk health status from SMART data.""" + device: str # e.g., /dev/nvme0n1 or /dev/sda + model: str + is_nvme: bool = True + + # Health status + healthy: bool = True + health_status: str = "" # "PASSED", "FAILED", etc. + + # Key metrics (NVMe) + percentage_used: Optional[int] = None # 0-100%, 100% = end of life + available_spare: Optional[int] = None # Spare capacity % + temperature: Optional[int] = None # Celsius + + # Usage stats (NVMe) - the actually useful stuff + data_written_tb: Optional[float] = None # TB written + power_on_hours: Optional[int] = None + power_cycles: Optional[int] = None + unsafe_shutdowns: Optional[int] = None + media_errors: Optional[int] = None + + # Key metrics (SATA) + reallocated_sectors: Optional[int] = None + pending_sectors: Optional[int] = None + + # Warnings + warnings: list[str] = field(default_factory=list) + + +@dataclass +class WiFiInfo: + """WiFi adapter information.""" + name: str + pci_id: str + vendor: str # Intel, MediaTek, etc. + driver: str = "" + + +@dataclass +class RFKillDevice: + """RF kill switch status for a wireless device.""" + index: int + name: str # e.g., "hci0", "phy0" + device_type: str # e.g., "Bluetooth", "Wireless LAN" + soft_blocked: bool = False + hard_blocked: bool = False + + +@dataclass +class WebcamInfo: + """Webcam and microphone detection. + + Framework webcam modules have separate hardware privacy switches for + camera and microphone: + + Gen 1 (OV2740/RTS5853): + Camera switch OFF → USB device electrically disconnected, disappears from bus + Mic switch OFF → microphones electrically disconnected + + Gen 2 (OV08X40/RTS5879): + Camera switch OFF → sensor powered down, controller stays alive, + sends blank frames. USB device stays on bus. + Mic switch OFF → microphones electrically disconnected + + Internal microphones route through mainboard audio codec (DMIC/HDA), + NOT through USB webcam. When mic switch is off, ALSA capture device + may still exist but records dead silence. + + We report what we can observe (device present/absent, capture devices) + and leave interpretation to the support agent. + """ + # Camera hardware + detected: bool = False + device_name: str = "" # from V4L2 sysfs, e.g. "Integrated Camera" + usb_id: str = "" # e.g. "32ac:001c" + usb_name: str = "" # from lsusb, e.g. "Framework Laptop Webcam Module (2nd Gen)" + v4l_devices: list[str] = field(default_factory=list) # e.g. ["/dev/video0"] + uvcvideo_loaded: bool = False + + # Microphone capture devices (from arecord -l) + mic_capture_devices: list[str] = field(default_factory=list) + + +@dataclass +class DisplayInfo: + """Connected display information.""" + connector: str # e.g., "eDP-1", "DP-1", "HDMI-A-1" + resolution: str = "" # e.g., "2256x1504" + refresh_rate: str = "" # e.g., "60.00" + is_internal: bool = False # True for eDP (laptop internal panel) + psr_status: str = "" # e.g., "PSR1 enabled", "disabled", "" if N/A + + +@dataclass +class RAMInfo: + """RAM information.""" + total_gb: int + ram_type: str = "" # DDR4, DDR5, etc. + speed_mhz: int = 0 + + +@dataclass +class FrameworkInfo: + """Framework-specific device information.""" + is_framework: bool = False + product_name: str = "" + model_version: str = "" + model_type: str = "" # "Laptop 13", "Laptop 16", "Laptop 12", "Desktop" + bios_version: str = "" + + # Power status + ac_connected: bool = False + battery_level: Optional[int] = None + battery_status: str = "" + battery_health_pct: Optional[float] = None # Actual capacity vs design capacity + + # Battery detail (sysfs + upower) + battery_cycle_count: Optional[int] = None + battery_design_wh: Optional[float] = None # Design capacity in Wh + battery_full_wh: Optional[float] = None # Current full charge capacity in Wh + battery_charge_rate_w: Optional[float] = None # Current charge/discharge rate in W + battery_charge_limit_pct: Optional[int] = None # Charge threshold if set (e.g., 80%) + + # Expansion cards + expansion_cards: list[str] = field(default_factory=list) + expansion_card_ports: list[tuple[str, str]] = field(default_factory=list) # (card_name, usb_port_path) + + +@dataclass +class HardwareInfo: + """Complete hardware information.""" + gpu: list[GPUInfo] = field(default_factory=list) + nvme: list[NVMeInfo] = field(default_factory=list) + disk_health: list[DiskHealthInfo] = field(default_factory=list) + wifi: Optional[WiFiInfo] = None + ram: Optional[RAMInfo] = None + framework: FrameworkInfo = field(default_factory=FrameworkInfo) + rfkill_devices: list[RFKillDevice] = field(default_factory=list) + webcam: Optional[WebcamInfo] = None + displays: list[DisplayInfo] = field(default_factory=list) + + cpu_vendor: CPUVendor = CPUVendor.UNKNOWN + cpu_model: str = "" + amd_generation: AMDGeneration = AMDGeneration.LEGACY + + +def _get_gpu_drivers_from_lshw() -> dict[str, str]: + """Get GPU driver mapping from lshw -C display. + + Returns dict of pci_id -> driver_name, e.g. {'c1:00.0': 'amdgpu'} + + lshw output looks like: + *-display + bus info: pci@0000:c1:00.0 + configuration: driver=amdgpu latency=0 + """ + drivers = {} + + rc, stdout, _ = run_command(['lshw', '-C', 'display']) + if rc != 0: + return drivers + + current_pci = '' + for line in stdout.split('\n'): + stripped = line.strip() + if stripped.startswith('bus info:') and 'pci@' in stripped: + # "bus info: pci@0000:c1:00.0" -> "c1:00.0" + pci_full = stripped.split('pci@')[-1].strip() + # Strip domain prefix: "0000:c1:00.0" -> "c1:00.0" + parts = pci_full.split(':') + if len(parts) >= 3: + current_pci = ':'.join(parts[1:]) # drop domain + else: + current_pci = pci_full + elif stripped.startswith('configuration:') and current_pci: + # "configuration: driver=amdgpu latency=0" + for token in stripped.split(): + if token.startswith('driver='): + drivers[current_pci] = token.split('=', 1)[1] + break + current_pci = '' + + return drivers + + +def detect_gpus() -> list[GPUInfo]: + """Detect GPU hardware using lspci for device info, lshw for drivers. + + Classifies GPUs as integrated vs discrete using PCI device class: + - "3D controller" (class 0302) → always discrete + - "VGA compatible controller" (class 0300) → integrated if a 3D controller + is also present, otherwise use vendor heuristics + - NVIDIA → always discrete on Framework hardware + + Driver detection via lshw -C display (configuration: driver=X). + """ + gpus = [] + + rc, stdout, _ = run_command(['lspci']) + if rc != 0: + return gpus + + # Collect GPU entries from lspci + entries = [] + for line in stdout.split('\n'): + if any(x in line for x in ['VGA compatible controller', + '3D controller', + 'Display controller']): + parts = line.split(' ', 1) + if len(parts) < 2: + continue + pci_id = parts[0] + desc = parts[1] + pci_class = desc.split(':')[0].strip() if ':' in desc else desc + entries.append({'pci_id': pci_id, 'class': pci_class, 'desc': desc}) + + # Get drivers from lshw + lshw_drivers = _get_gpu_drivers_from_lshw() + + # Check if any entry is a "3D controller" — means system has iGPU + dGPU + has_3d_controller = any('3D controller' in e['class'] for e in entries) + + for e in entries: + desc = e['desc'] + vendor = 'Unknown' + if 'AMD' in desc or 'ATI' in desc: + vendor = 'AMD' + elif 'Intel' in desc: + vendor = 'Intel' + elif 'NVIDIA' in desc: + vendor = 'NVIDIA' + + if '3D controller' in e['class']: + is_discrete = True + elif vendor == 'NVIDIA': + is_discrete = True + elif has_3d_controller: + is_discrete = False + else: + is_discrete = vendor not in ('Intel', 'AMD') + + driver = lshw_drivers.get(e['pci_id'], '') + + gpus.append(GPUInfo( + name=desc, + pci_id=e['pci_id'], + vendor=vendor, + driver=driver, + is_discrete=is_discrete, + )) + + return gpus + + +def detect_nvme_devices() -> list[NVMeInfo]: + """ + Detect NVMe storage devices. + + BUG FIX: Improved model extraction using proper sed pattern + """ + devices = [] + + # First get PCI info + rc, stdout, _ = run_command(['lspci']) + if rc != 0: + return devices + + pci_nvme = {} + for line in stdout.split('\n'): + if 'non-volatile' in line.lower() or 'nvme' in line.lower(): + parts = line.split(' ', 1) + if len(parts) >= 2: + pci_nvme[parts[0]] = parts[1] + + # Now get device details + nvme_path = Path('/dev') + for nvme in sorted(nvme_path.glob('nvme*n*')): + # Skip partition devices (nvme0n1p1, etc.) and controller-only paths (nvme0) + if not re.match(r'nvme\d+n\d+$', nvme.name): + continue + device_name = nvme.name + model = "" + firmware = "" + + # Get model using nvme id-ctrl + # BUG FIX: Proper model extraction + rc, stdout, _ = run_sudo_command(['nvme', 'id-ctrl', str(nvme)]) + if rc == 0: + for line in stdout.split('\n'): + # Match "mn : Model Name Here" + if line.startswith('mn'): + # Extract value after colon, strip whitespace + match = re.match(r'^mn\s*:\s*(.+)$', line) + if match: + model = match.group(1).strip() + elif line.startswith('fr'): + match = re.match(r'^fr\s*:\s*(.+)$', line) + if match: + firmware = match.group(1).strip() + + devices.append(NVMeInfo( + device=str(nvme), + model=model or "(model not detected)", + firmware=firmware + )) + + return devices + + +def check_disk_health(known_nvme_models: Optional[dict[str, str]] = None) -> list[DiskHealthInfo]: + """ + Check disk health using nvme-cli and smartctl. + + Args: + known_nvme_models: Optional dict of device_path -> model_name from + detect_nvme_devices(), avoids duplicate sudo nvme id-ctrl calls. + + Returns health info for all detected drives. + """ + if known_nvme_models is None: + known_nvme_models = {} + + health_info = [] + + # Check NVMe drives + nvme_devices = list(Path('/dev').glob('nvme[0-9]*n[0-9]*')) + # Filter out partition devices (nvme0n1p1, etc.) + nvme_devices = [d for d in nvme_devices if re.match(r'nvme\d+n\d+$', d.name)] + + for nvme in nvme_devices: + device = str(nvme) + info = DiskHealthInfo(device=device, model="", is_nvme=True) + + # Reuse model from detect_nvme_devices() if available + if device in known_nvme_models: + info.model = known_nvme_models[device] + else: + # Get model name (only if not already detected) + rc, stdout, _ = run_sudo_command(['nvme', 'id-ctrl', device]) + if rc == 0: + for line in stdout.split('\n'): + if line.startswith('mn'): + match = re.match(r'^mn\s*:\s*(.+)$', line) + if match: + info.model = match.group(1).strip() + break + + # Get SMART health + rc, stdout, _ = run_sudo_command(['nvme', 'smart-log', device]) + if rc == 0: + for line in stdout.split('\n'): + line = line.lower().strip() + + # Critical warning + if 'critical_warning' in line or 'critical warning' in line: + match = re.search(r':\s*(\d+)', line) + if match and int(match.group(1)) != 0: + info.healthy = False + info.warnings.append(f"Critical warning flag: {match.group(1)}") + + # Percentage used (0-100%, higher = more worn) + if info.percentage_used is None: + if 'percentage_used' in line or 'percentage used' in line: + match = re.search(r':\s*(\d+)', line) + if match: + info.percentage_used = int(match.group(1)) + if info.percentage_used >= 90: + info.warnings.append(f"Drive is {info.percentage_used}% worn - replace soon") + info.healthy = False + elif info.percentage_used >= 80: + info.warnings.append(f"Drive is {info.percentage_used}% worn - monitor closely") + + # Available spare (NOT the threshold - that's a different field) + # nvme smart-log shows: "available_spare : 100%" and "available_spare_threshold : 10%" + # Only match lines starting with "available_spare" (not "available_spare_threshold") + if info.available_spare is None: + spare_match = re.match(r'^available_spare\s*:\s*(\d+)', line) + if spare_match: + info.available_spare = int(spare_match.group(1)) + if info.available_spare <= 10: + info.warnings.append(f"Low SSD spare blocks: {info.available_spare}% (drive wear-leveling reserve nearly exhausted)") + info.healthy = False + + # Temperature - be very specific to avoid matching thresholds or sensors + # Formats seen: + # "temperature : 122 °F (323 K)" - Fahrenheit with Kelvin + # "temperature : 308 K (35 Celsius)" - Kelvin with Celsius + # "temperature : 35" - Just a number (assume Celsius if <100, Kelvin if >200) + # Avoid: "Temperature Sensor 1", "Warning Temperature Time", etc. + if info.temperature is None: + # Match line that starts with just "temperature" followed by colon + temp_match = re.match(r'^temperature\s*:', line) + if temp_match: + # Try Celsius first (most reliable) + celsius_match = re.search(r'\((\d+)\s*[Cc]elsius\)', line) + if celsius_match: + info.temperature = int(celsius_match.group(1)) + else: + # Try Fahrenheit: "122 °F" or "122°F" + fahrenheit_match = re.search(r':\s*(\d+)\s*°?[Ff]', line) + if fahrenheit_match: + temp_f = int(fahrenheit_match.group(1)) + info.temperature = int((temp_f - 32) * 5 / 9) + else: + # Try Kelvin in parentheses: "(323 K)" + kelvin_match = re.search(r'\((\d+)\s*K\)', line) + if kelvin_match: + temp_k = int(kelvin_match.group(1)) + info.temperature = temp_k - 273 + else: + # Fall back to raw number after colon + num_match = re.search(r':\s*(\d+)', line) + if num_match: + temp = int(num_match.group(1)) + # Heuristic: >200 is Kelvin, <100 is Celsius + if temp > 200: + info.temperature = temp - 273 + elif temp < 100: + info.temperature = temp + + # Data Units Written - "Data Units Written : 15310335 (7.84 TB)" + if 'data units written' in line: + # Try to get TB value from parentheses + tb_match = re.search(r'\((\d+\.?\d*)\s*TB\)', line, re.IGNORECASE) + if tb_match: + info.data_written_tb = float(tb_match.group(1)) + else: + # Fall back to calculating from units (1 unit = 512KB * 1000 = 512MB) + units_match = re.search(r':\s*(\d+)', line) + if units_match: + units = int(units_match.group(1)) + info.data_written_tb = round(units * 512 * 1000 / (1024**4), 2) + + # Power on hours + if 'power_on_hours' in line or 'power on hours' in line: + match = re.search(r':\s*(\d+)', line) + if match: + info.power_on_hours = int(match.group(1)) + + # Power cycles + if 'power_cycles' in line or 'power cycles' in line: + match = re.search(r':\s*(\d+)', line) + if match: + info.power_cycles = int(match.group(1)) + + # Unsafe shutdowns (hard power-offs, not a big deal unless excessive) + if 'unsafe_shutdowns' in line or 'unsafe shutdowns' in line: + match = re.search(r':\s*(\d+)', line) + if match: + info.unsafe_shutdowns = int(match.group(1)) + + # Media errors + if 'media_errors' in line or 'media errors' in line: + match = re.search(r':\s*(\d+)', line) + if match: + info.media_errors = int(match.group(1)) + if info.media_errors > 0: + info.healthy = False + info.warnings.append(f"Media errors detected: {info.media_errors}") + + info.health_status = "PASSED" if info.healthy else "WARNING" + health_info.append(info) + + # Check SATA drives with smartctl + sata_devices = list(Path('/dev').glob('sd[a-z]')) + for sata in sata_devices: + device = str(sata) + info = DiskHealthInfo(device=device, model="", is_nvme=False) + + # Get SMART health + rc, stdout, _ = run_sudo_command(['smartctl', '-H', '-A', '-i', device]) + if rc == 0 or rc == 4: # rc=4 means SMART threshold exceeded + for line in stdout.split('\n'): + # Model + if 'Device Model' in line or 'Model Number' in line: + parts = line.split(':') + if len(parts) >= 2: + info.model = parts[1].strip() + + # Overall health + if 'SMART overall-health' in line or 'SMART Health Status' in line: + info.health_status = "PASSED" if 'PASSED' in line or 'OK' in line else "FAILED" + if 'FAILED' in line: + info.healthy = False + info.warnings.append("SMART health check FAILED") + + # Reallocated sectors (ID 5) + if 'Reallocated_Sector' in line: + match = re.search(r'(\d+)\s*$', line) + if match: + info.reallocated_sectors = int(match.group(1)) + if info.reallocated_sectors > 0: + info.warnings.append(f"Reallocated sectors: {info.reallocated_sectors}") + if info.reallocated_sectors > 100: + info.healthy = False + + # Pending sectors (ID 197) + if 'Current_Pending_Sector' in line: + match = re.search(r'(\d+)\s*$', line) + if match: + info.pending_sectors = int(match.group(1)) + if info.pending_sectors > 0: + info.warnings.append(f"Pending sectors: {info.pending_sectors}") + info.healthy = False + + # Temperature + if 'Temperature_Celsius' in line or 'Airflow_Temperature' in line: + match = re.search(r'(\d+)(?:\s+\(|\s*$)', line) + if match: + info.temperature = int(match.group(1)) + + if not info.health_status: + info.health_status = "UNKNOWN" + + if info.model: # Only add if we got some info + health_info.append(info) + + return health_info + + +def detect_wifi() -> Optional[WiFiInfo]: + """Detect WiFi adapter.""" + rc, stdout, _ = run_command(['lspci']) + if rc != 0: + return None + + # WiFi detection patterns + wifi_patterns = [ + r'wireless', r'wifi', r'802\.11', + r'network controller.*wi-fi', + r'network controller.*MT79', # MediaTek + r'network controller.*intel', + r'network controller.*realtek', + r'network controller.*broadcom', + r'network controller.*mediatek', + ] + + for line in stdout.split('\n'): + line_lower = line.lower() + for pattern in wifi_patterns: + if re.search(pattern, line_lower): + parts = line.split(' ', 1) + if len(parts) >= 2: + pci_id = parts[0] + description = parts[1] + + vendor = 'Unknown' + if 'intel' in line_lower: + vendor = 'Intel' + elif 'mediatek' in line_lower or 'mt79' in line_lower: + vendor = 'MediaTek' + elif 'realtek' in line_lower: + vendor = 'Realtek' + elif 'broadcom' in line_lower: + vendor = 'Broadcom' + + return WiFiInfo( + name=description, + pci_id=pci_id, + vendor=vendor + ) + + return None + + +def detect_ram() -> Optional[RAMInfo]: + """Detect RAM information using dmidecode.""" + total_mb = 0 + ram_type = "" + speed = 0 + + # Try dmidecode first + rc, stdout, _ = run_sudo_command(['dmidecode', '-t', 'memory']) + if rc == 0: + for line in stdout.split('\n'): + line = line.strip() + + # Parse size lines + if line.startswith('Size:') and 'No Module' not in line and 'Not Specified' not in line: + match = re.search(r'(\d+)\s*(MB|GB)', line) + if match: + size = int(match.group(1)) + unit = match.group(2) + if unit == 'GB': + size *= 1024 + total_mb += size + + # Parse type + elif line.startswith('Type:') and 'Unknown' not in line and 'Error' not in line: + ram_type = line.split(':')[1].strip() + + # Parse speed + elif 'Configured Memory Speed:' in line: + match = re.search(r'(\d+)', line) + if match: + speed = int(match.group(1)) + + # Fallback to /proc/meminfo + if total_mb == 0: + try: + with open('/proc/meminfo') as f: + for line in f: + if line.startswith('MemTotal:'): + match = re.search(r'(\d+)', line) + if match: + total_mb = int(match.group(1)) // 1024 # KB to MB + break + except Exception: + pass + + if total_mb > 0: + return RAMInfo( + total_gb=total_mb // 1024, + ram_type=ram_type, + speed_mhz=speed + ) + + return None + + +def detect_rfkill() -> list[RFKillDevice]: + """ + Detect RF kill switch status for wireless devices. + + Parses output from 'rfkill list' command. + """ + devices = [] + + rc, stdout, _ = run_command(['rfkill', 'list']) + if rc != 0: + return devices + + current_device = None + + for line in stdout.split('\n'): + # New device line: "0: hci0: Bluetooth" + device_match = re.match(r'^(\d+):\s+(\S+):\s+(.+)$', line) + if device_match: + if current_device: + devices.append(current_device) + current_device = RFKillDevice( + index=int(device_match.group(1)), + name=device_match.group(2), + device_type=device_match.group(3).strip() + ) + elif current_device: + # Soft blocked line + if 'Soft blocked:' in line: + current_device.soft_blocked = 'yes' in line.lower() + # Hard blocked line + elif 'Hard blocked:' in line: + current_device.hard_blocked = 'yes' in line.lower() + + # Don't forget the last device + if current_device: + devices.append(current_device) + + return devices + + +# Known Framework webcam USB IDs (vendor:product) +_FRAMEWORK_WEBCAM_IDS = { + '32ac:001c': ('Framework Laptop Webcam Module (2nd Gen)', 2), + '04f2:b6d9': ('Chicony Integrated Camera', 1), # FW13 1st gen +} + +# Framework vendor ID for matching any future webcam modules +_FRAMEWORK_USB_VENDOR = '32ac' + + +def _detect_alsa_capture_devices() -> list[str]: + """Detect ALSA capture devices via arecord -l. + + Returns list of capture device descriptions like: + "card 0: PCH [HDA Intel PCH], device 0: ALC295 Analog [ALC295 Analog]" + + On Framework, internal mics route through mainboard audio codec (DMIC/HDA), + not USB. When mic hardware switch is OFF, mics are electrically disconnected + and may not appear as capture sources. + """ + devices = [] + rc, stdout, _ = run_command(['arecord', '-l']) + if rc != 0: + return devices + + for line in stdout.split('\n'): + if line.startswith('card '): + devices.append(line.strip()) + + return devices + + +def detect_webcam(is_framework: bool = False) -> WebcamInfo: + """Detect webcam presence and mic capture devices. + + Detection approach: + 1. /sys/class/video4linux/ — V4L2 video devices present? + 2. Read device name from sysfs + 3. lsusb — known Framework webcam USB IDs + 4. uvcvideo module loaded? + 5. arecord -l — ALSA capture devices (mic hardware presence) + + Reports observable facts only. Does NOT attempt to determine + privacy switch state — too many false positives without verified + hardware behavior for gen 1 (USB removal vs module absent) and + gen 2 (UVC privacy control semantics unverified). + """ + info = WebcamInfo() + + # Check V4L2 devices via sysfs + v4l_path = Path('/sys/class/video4linux') + if v4l_path.exists(): + for dev in sorted(v4l_path.iterdir()): + info.v4l_devices.append(f'/dev/{dev.name}') + # Read device name + name_file = dev / 'name' + try: + if name_file.exists(): + name = name_file.read_text().strip() + if name and not info.device_name: + info.device_name = name + except (OSError, PermissionError): + pass + + # Check uvcvideo module + rc, stdout, _ = run_command(['lsmod']) + if rc == 0: + for line in stdout.split('\n'): + if line.startswith('uvcvideo '): + info.uvcvideo_loaded = True + break + + # Check lsusb for known webcam devices + rc, stdout, _ = run_command(['lsusb']) + if rc == 0: + for line in stdout.split('\n'): + # Check known Framework webcam IDs + for usb_id, (name, _gen) in _FRAMEWORK_WEBCAM_IDS.items(): + if usb_id in line: + info.usb_id = usb_id + info.usb_name = name + break + # Also check for any Framework vendor USB device with camera class + if not info.usb_id and f'{_FRAMEWORK_USB_VENDOR}:' in line: + if any(kw in line.lower() for kw in ['webcam', 'camera']): + parts = line.split('ID ') + if len(parts) > 1: + info.usb_id = parts[1].split()[0] + info.usb_name = parts[1].split(' ', 1)[1].strip() if ' ' in parts[1] else '' + + # Determine detection status + info.detected = bool(info.v4l_devices) or bool(info.usb_id) + + # Mic capture device detection + info.mic_capture_devices = _detect_alsa_capture_devices() + + return info + + +def detect_cpu_info() -> tuple[CPUVendor, str, AMDGeneration]: + """Detect CPU vendor, model, and AMD generation.""" + vendor = CPUVendor.UNKNOWN + model = "" + amd_gen = AMDGeneration.LEGACY + + rc, stdout, _ = run_command(['lscpu']) + if rc != 0: + return vendor, model, amd_gen + + for line in stdout.split('\n'): + if 'Vendor ID:' in line: + if 'AMD' in line: + vendor = CPUVendor.AMD + elif 'Intel' in line: + vendor = CPUVendor.INTEL + elif 'Model name:' in line: + model = line.split(':', 1)[1].strip() + + # Detect AMD generation for thermal thresholds + if vendor == CPUVendor.AMD and model: + # Modern AMD: Ryzen 7000/8000 series, AI 300 series + if re.search(r'7[4-9]\d\d|8\d{3}|AI\s', model): + amd_gen = AMDGeneration.MODERN + + return vendor, model, amd_gen + + +def detect_framework_info() -> FrameworkInfo: + """Detect Framework-specific hardware information.""" + info = FrameworkInfo() + + # Get product name and version + rc, stdout, _ = run_sudo_command(['dmidecode', '-s', 'system-product-name']) + if rc == 0: + info.product_name = stdout.strip() + + rc, stdout, _ = run_sudo_command(['dmidecode', '-s', 'system-version']) + if rc == 0: + info.model_version = stdout.strip() + + rc, stdout, _ = run_sudo_command(['dmidecode', '-s', 'bios-version']) + if rc == 0: + info.bios_version = stdout.strip() + + # Check if Framework device + framework_indicators = ['Framework', 'Laptop Pro', 'Laptop 13', 'Laptop 16', 'Laptop 12', 'Desktop'] + if any(ind in info.product_name for ind in framework_indicators): + info.is_framework = True + + # Determine model type + if ('Laptop Pro' in info.product_name or 'Laptop 13 Pro' in info.product_name + or 'Laptop Pro' in info.model_version or 'Laptop 13 Pro' in info.model_version): + info.model_type = 'Laptop Pro' + elif 'Laptop 13' in info.product_name or 'Laptop 13' in info.model_version: + info.model_type = 'Laptop 13' + elif 'Laptop 16' in info.product_name or 'Laptop 16' in info.model_version: + info.model_type = 'Laptop 16' + elif 'Laptop 12' in info.product_name or 'Laptop 12' in info.model_version: + info.model_type = 'Laptop 12' + elif 'Desktop' in info.product_name or 'Desktop' in info.model_version: + info.model_type = 'Desktop' + + if info.is_framework: + # Power status + _detect_power_status(info) + + # Expansion cards + _detect_expansion_cards(info) + + return info + + +def _detect_power_status(info: FrameworkInfo): + """Detect power and battery status.""" + # Try multiple AC paths + ac_paths = list(Path('/sys/class/power_supply').glob('ADP*/online')) + \ + list(Path('/sys/class/power_supply').glob('AC*/online')) + + for ac_path in ac_paths: + try: + status = ac_path.read_text().strip() + info.ac_connected = (status == '1') + break + except Exception: + pass + + # Battery + bat_paths = list(Path('/sys/class/power_supply').glob('BAT*')) + for bat_path in bat_paths: + try: + capacity_file = bat_path / 'capacity' + status_file = bat_path / 'status' + + if capacity_file.exists(): + info.battery_level = int(capacity_file.read_text().strip()) + if status_file.exists(): + info.battery_status = status_file.read_text().strip() + + # Cycle count (direct sysfs read) + cycle_file = bat_path / 'cycle_count' + if cycle_file.exists(): + val = cycle_file.read_text().strip() + if val.isdigit(): + info.battery_cycle_count = int(val) + + # Charge rate in watts (power_now is in microwatts) + power_file = bat_path / 'power_now' + if power_file.exists(): + val = power_file.read_text().strip() + if val.isdigit(): + info.battery_charge_rate_w = round(int(val) / 1_000_000, 1) + + # Charge threshold (Framework EC exposes this) + threshold_file = bat_path / 'charge_control_end_threshold' + if threshold_file.exists(): + val = threshold_file.read_text().strip() + if val.isdigit(): + threshold = int(val) + # Only report if not default (100 = no limit set) + if threshold < 100: + info.battery_charge_limit_pct = threshold + + break + except Exception: + pass + + # Battery health + capacities using upower (more reliable than sysfs for Wh) + rc, stdout, _ = run_command(['upower', '-e']) + if rc == 0: + for line in stdout.split('\n'): + if 'battery' in line.lower(): + rc2, stdout2, _ = run_command(['upower', '-i', line.strip()]) + if rc2 == 0: + energy_full = None + energy_design = None + + for uline in stdout2.split('\n'): + if 'energy-full:' in uline and 'design' not in uline: + match = re.search(r'([\d.]+)', uline) + if match: + energy_full = float(match.group(1)) + elif 'energy-full-design:' in uline: + match = re.search(r'([\d.]+)', uline) + if match: + energy_design = float(match.group(1)) + + if energy_full: + info.battery_full_wh = round(energy_full, 1) + if energy_design: + info.battery_design_wh = round(energy_design, 1) + if energy_full and energy_design: + health = (energy_full / energy_design) * 100 + info.battery_health_pct = round(health, 1) + break + + +def _detect_expansion_cards(info: FrameworkInfo): + """Detect Framework expansion cards and input modules using lsusb. + + Framework uses vendor ID 32ac for their branded hardware: + + Expansion Cards (FW13 + FW16): + - 32ac:0002 - HDMI Expansion Card (Parade PS186 DP-to-HDMI converter) + - 32ac:0003 - DisplayPort Expansion Card + - 32ac:0010 - Audio Expansion Card (Conexant CX31993 DAC) + + Input Modules (FW16 only): + - 32ac:0012 - Keyboard (ANSI) + - 32ac:0013 - RGB Macropad + - 32ac:0014 - Numpad + - 32ac:0018 - Keyboard (ISO) + - 32ac:0019 - Keyboard (JIS) + - 32ac:0020 - LED Matrix + + Third-party chips in Framework expansion cards: + - Ethernet: Realtek RTL8156 (0bda:8156) - 2.5Gbit + + Passive cards (no USB ID - won't appear in lsusb): + - USB-A Expansion Card (passive, no chip) + - USB-C Expansion Card (passive passthrough) + + Generic controllers (appear but not Framework-branded): + - Storage Expansion Card (appears as generic USB storage) + - MicroSD/SD Expansion Card (appears as generic card reader) + """ + rc, stdout, _ = run_command(['lsusb']) + if rc != 0: + return + + # Framework-branded devices (vendor ID 32ac) + FRAMEWORK_USB_IDS = { + # Expansion Cards + '32ac:0002': 'HDMI Expansion Card', + '32ac:0003': 'DisplayPort Expansion Card', + '32ac:0010': 'Audio Expansion Card', # Conexant CX31993 + # Input Modules (FW16) + '32ac:0012': 'Keyboard (ANSI)', + '32ac:0013': 'RGB Macropad', + '32ac:0014': 'Numpad', + '32ac:0018': 'Keyboard (ISO)', + '32ac:0019': 'Keyboard (JIS)', + '32ac:0020': 'LED Matrix', + } + + # Third-party chips used in Framework expansion cards + # These appear with the chip vendor's ID, not Framework's + EXPANSION_CARD_CHIPS = { + '0bda:8156': 'Ethernet Expansion Card (2.5G)', # Realtek RTL8156 + } + + # Build bus:device -> USB port path mapping from sysfs + # sysfs encodes physical topology: e.g. "3-1.2" = bus 3, port 1, sub-port 2 + usb_port_map = _build_usb_port_map() + + for line in stdout.split('\n'): + # lsusb format: "Bus 001 Device 002: ID 32ac:0002 Framework HDMI Expansion Card" + match = re.search(r'Bus\s+(\d+)\s+Device\s+(\d+):\s+ID\s+([0-9a-f]{4}:[0-9a-f]{4})', line, re.IGNORECASE) + if match: + bus = match.group(1) + device = match.group(2) + usb_id = match.group(3).lower() + + card_name = FRAMEWORK_USB_IDS.get(usb_id) or EXPANSION_CARD_CHIPS.get(usb_id) + if card_name: + if card_name not in info.expansion_cards: + info.expansion_cards.append(card_name) + + # Look up USB port path for this device + bus_dev_key = f"{int(bus)}:{int(device)}" + port_path = usb_port_map.get(bus_dev_key, "") + if port_path: + info.expansion_card_ports.append((card_name, port_path)) + + +def _build_usb_port_map() -> dict[str, str]: + """Build mapping of bus:device_num -> USB port path from sysfs. + + Reads /sys/bus/usb/devices/*/devnum and busnum to match lsusb output + to sysfs device paths. The sysfs directory name encodes the physical + USB port topology (e.g. '3-1.2' = bus 3, root port 1, hub port 2). + + Returns: + Dict of 'bus:device' -> 'port_path' (e.g. '3:5' -> '3-1.2') + """ + port_map = {} + usb_devices = Path('/sys/bus/usb/devices') + + if not usb_devices.exists(): + return port_map + + for dev_dir in usb_devices.iterdir(): + try: + busnum_file = dev_dir / 'busnum' + devnum_file = dev_dir / 'devnum' + + if busnum_file.exists() and devnum_file.exists(): + busnum = busnum_file.read_text().strip() + devnum = devnum_file.read_text().strip() + key = f"{busnum}:{devnum}" + # Use the sysfs directory name as the port path + port_map[key] = dev_dir.name + except Exception: + pass + + return port_map + + +def _enrich_from_debugfs(displays: list[DisplayInfo], cpu_vendor: str = '') -> None: + """Fill in PSR status and missing refresh rates from DRM debugfs. + + debugfs requires root (which we have — tool runs under sudo). + + Intel: /sys/kernel/debug/dri/*/i915_edp_psr_status + /sys/kernel/debug/dri/*/i915_display_info + AMD: /sys/kernel/debug/dri/*/eDP-*/psr_state + /sys/kernel/debug/dri/*/amdgpu_current_backlight_pwm (as probe) + """ + debugfs = Path('/sys/kernel/debug/dri') + if not debugfs.exists(): + return + + # --- PSR status (eDP only) --- + edp_displays = [d for d in displays if d.is_internal] + if edp_displays: + _detect_psr_intel(debugfs, edp_displays) + _detect_psr_amd(debugfs, edp_displays) + + # --- Refresh rate from debugfs when missing --- + missing_refresh = [d for d in displays if not d.refresh_rate] + if missing_refresh: + _detect_refresh_from_debugfs(debugfs, missing_refresh) + + +def _detect_psr_intel(debugfs: Path, edp_displays: list[DisplayInfo]) -> None: + """Detect Intel PSR status from i915 debugfs. + + File: /sys/kernel/debug/dri/*/i915_edp_psr_status + + Typical contents when enabled: + Sink support: yes [0x01] + PSR mode: PSR1 enabled + Source PSR ctl: enabled [0x81f00e26] + + When disabled: + Sink support: yes [0x01] + PSR mode: disabled + ... + + When not supported: + Sink support: no + """ + for card_dir in sorted(debugfs.iterdir()): + psr_file = card_dir / 'i915_edp_psr_status' + if not psr_file.exists(): + continue + + try: + content = psr_file.read_text() + except (OSError, PermissionError): + # debugfs might need root even with sudo if securelevel is high + rc, content, _ = run_sudo_command(['cat', str(psr_file)]) + if rc != 0: + continue + + sink_support = False + psr_mode = "" + + for line in content.split('\n'): + line_lower = line.strip().lower() + if 'sink support:' in line_lower: + sink_support = 'yes' in line_lower + if 'psr mode:' in line_lower: + # "PSR mode: PSR1 enabled" or "PSR mode: disabled" + psr_mode = line.split(':', 1)[1].strip() + + if not sink_support: + status = "not supported (sink)" + elif psr_mode: + status = psr_mode + else: + status = "supported (state unknown)" + + for d in edp_displays: + if not d.psr_status: # Don't overwrite if already set + d.psr_status = status + + break # Only one i915 PSR status file per system + + +def _detect_psr_amd(debugfs: Path, edp_displays: list[DisplayInfo]) -> None: + """Detect AMD PSR status from amdgpu debugfs. + + Newer kernels (6.x): /sys/kernel/debug/dri/*/eDP-1/psr_state + Shows numeric state: 0=disabled, 1-5=various active states + + Also check: /sys/kernel/debug/dri/*/amdgpu_dm_psr_state (older path) + """ + for card_dir in sorted(debugfs.iterdir()): + # Method 1: Per-connector psr_state (newer kernels) + for edp_dir in sorted(card_dir.glob('eDP-*')): + psr_file = edp_dir / 'psr_state' + if not psr_file.exists(): + continue + + try: + content = psr_file.read_text().strip() + except (OSError, PermissionError): + rc, content, _ = run_sudo_command(['cat', str(psr_file)]) + if rc != 0: + continue + content = content.strip() + + # Numeric state: 0 = disabled, 1+ = enabled in various states + connector_name = edp_dir.name # e.g., "eDP-1" + try: + state = int(content) + status = "enabled" if state > 0 else "disabled" + except ValueError: + status = content # Pass through whatever it says + + for d in edp_displays: + if not d.psr_status and d.connector == connector_name: + d.psr_status = status + return + + # Method 2: Global amdgpu_dm_psr_state (older kernels) + global_psr = card_dir / 'amdgpu_dm_psr_state' + if global_psr.exists(): + try: + content = global_psr.read_text().strip() + except (OSError, PermissionError): + rc, content, _ = run_sudo_command(['cat', str(global_psr)]) + if rc != 0: + continue + content = content.strip() + + try: + state = int(content) + status = "enabled" if state > 0 else "disabled" + except ValueError: + status = content + + for d in edp_displays: + if not d.psr_status: + d.psr_status = status + return + + +def _detect_refresh_from_debugfs(debugfs: Path, displays: list[DisplayInfo]) -> None: + """Fill in missing refresh rates from DRM debugfs. + + Intel: /sys/kernel/debug/dri/*/i915_display_info + Contains lines like: + pipe A...mode="2256x1504": 60 ... + + AMD: /sys/kernel/debug/dri/*/state + Contains lines like: + mode=...2560x1600...vrefresh=165 + + Falls back to modetest -c if debugfs doesn't work. + """ + # Build lookup of connectors needing refresh + need_refresh = {d.connector: d for d in displays} + + for card_dir in sorted(debugfs.iterdir()): + if not card_dir.is_dir(): + continue + + # Intel: i915_display_info + display_info = card_dir / 'i915_display_info' + if display_info.exists(): + try: + content = display_info.read_text() + except (OSError, PermissionError): + rc, content, _ = run_sudo_command(['cat', str(display_info)]) + if rc != 0: + content = "" + + if content: + _parse_intel_display_info(content, need_refresh) + if not need_refresh: + return + + # AMD: per-connector state files or global state + state_file = card_dir / 'state' + if state_file.exists(): + try: + content = state_file.read_text() + except (OSError, PermissionError): + # This file can be huge, skip if can't read + content = "" + + if content: + _parse_amd_state(content, need_refresh) + if not need_refresh: + return + + # Last resort: modetest -c (from libdrm) + if need_refresh: + _try_modetest(need_refresh) + + +def _parse_intel_display_info(content: str, need_refresh: dict[str, DisplayInfo]) -> None: + """Parse i915_display_info for active mode refresh rates. + + Look for patterns like: + [CONNECTOR:236:eDP-1]: ... + ... + crtc = (C1) ... "2256x1504": 60 267956 2256 2264 2296 2368 1504 ... + + Or in newer kernels: + [CRTC:51:pipe A]: + ... + active=yes, mode="2256x1504": 60 ... + """ + current_connector = None + + for line in content.split('\n'): + # Track current connector + conn_match = re.search(r'\[CONNECTOR:\d+:(\S+)\]', line) + if conn_match: + name = conn_match.group(1) + current_connector = name if name in need_refresh else None + continue + + # Look for mode with refresh rate near a connector or pipe section + # Pattern: "2256x1504": 60 or mode="2256x1504": 60 + if current_connector: + mode_match = re.search(r'"(\d+x\d+)":\s+(\d+)', line) + if mode_match: + resolution = mode_match.group(1) + refresh = mode_match.group(2) + disp = need_refresh[current_connector] + if disp.resolution == resolution or not disp.resolution: + disp.refresh_rate = refresh + del need_refresh[current_connector] + current_connector = None + if not need_refresh: + return + + +def _parse_amd_state(content: str, need_refresh: dict[str, DisplayInfo]) -> None: + """Parse amdgpu state file for vrefresh. + + Look for patterns like: + connector[67]: ... name=eDP-1 ... + ... + vrefresh=165 + """ + current_connector = None + + for line in content.split('\n'): + # Connector line + conn_match = re.search(r'name=(eDP-\d+|DP-\d+|HDMI-A-\d+)', line) + if conn_match: + name = conn_match.group(1) + current_connector = name if name in need_refresh else None + continue + + if current_connector: + # vrefresh in mode line + vr_match = re.search(r'vrefresh=(\d+)', line) + if vr_match: + need_refresh[current_connector].refresh_rate = vr_match.group(1) + del need_refresh[current_connector] + current_connector = None + if not need_refresh: + return + + +def _try_modetest(need_refresh: dict[str, DisplayInfo]) -> None: + """Last-resort: use modetest -c from libdrm for refresh rates. + + Output format: + Connectors: + id encoder status name size (mm) modes encoders + 236 235 connected eDP-1 285x190 3 235 + modes: + index name refresh (Hz) ... + #0 2256x1504 59.99 ... + """ + rc, stdout, _ = run_command(['modetest', '-c'], timeout=5) + if rc != 0: + return + + current_connector = None + in_modes = False + + for line in stdout.split('\n'): + # Connector header line: "236 235 connected eDP-1 ..." + conn_match = re.match(r'^\d+\s+\d+\s+connected\s+(\S+)', line) + if conn_match: + name = conn_match.group(1) + current_connector = name if name in need_refresh else None + in_modes = False + continue + + if current_connector and 'modes:' in line: + in_modes = True + continue + + # First mode line after "modes:": "#0 2256x1504 59.99 ..." + if current_connector and in_modes: + mode_match = re.match(r'\s+#0\s+(\S+)\s+([\d.]+)', line) + if mode_match: + refresh = mode_match.group(2) + need_refresh[current_connector].refresh_rate = refresh + del need_refresh[current_connector] + current_connector = None + in_modes = False + if not need_refresh: + return + + +def detect_displays() -> list[DisplayInfo]: + """Detect connected displays with resolution and refresh rate. + + Three-layer approach: + 1. Try xrandr --current (works on X11 and Wayland with XWayland, which + is the default on GNOME, KDE, etc.) + 2. Fall back to sysfs /sys/class/drm/ for connected outputs and preferred + modes (works everywhere but only shows preferred mode, not necessarily + the active one) + 3. Enrich from DRM debugfs: fill in missing refresh rates + PSR status + + For Framework laptops: + - eDP = internal panel (Laptop 13: 2256x1504@60, Laptop 16: 2560x1600@165, + Laptop 12: 2880x1920@120) + - DP-* = DisplayPort expansion card or USB-C dock + - HDMI-* = HDMI expansion card + """ + displays = [] + + # Method 1: xrandr --current + # Need to find the right DISPLAY/XAUTHORITY for sudo context + display_env = {} + sudo_user = os.environ.get('SUDO_USER', '') + + if sudo_user: + # Running as sudo — need to pass through display environment + # Try common DISPLAY values + for display_var in [os.environ.get('DISPLAY', ''), ':0', ':1']: + if display_var: + display_env['DISPLAY'] = display_var + break + + # XAUTHORITY for X11 + xauth = os.environ.get('XAUTHORITY', '') + if not xauth and sudo_user: + # Common locations + for candidate in [ + f'/home/{sudo_user}/.Xauthority', + f'/run/user/{os.environ.get("SUDO_UID", "1000")}/.Xauthority', + ]: + if Path(candidate).exists(): + xauth = candidate + break + if xauth: + display_env['XAUTHORITY'] = xauth + + # WAYLAND_DISPLAY for XWayland + wayland = os.environ.get('WAYLAND_DISPLAY', '') + if wayland: + display_env['WAYLAND_DISPLAY'] = wayland + + xdg_runtime = os.environ.get('XDG_RUNTIME_DIR', '') + if not xdg_runtime: + xdg_runtime = f'/run/user/{os.environ.get("SUDO_UID", "1000")}' + display_env['XDG_RUNTIME_DIR'] = xdg_runtime + + # Try xrandr with display env + env = {**os.environ, **display_env} if display_env else None + rc, stdout, _ = run_command(['xrandr', '--current'], env=env) + + if rc == 0 and stdout.strip(): + displays = _parse_xrandr(stdout) + + # Method 2: sysfs fallback if xrandr failed or returned nothing + if not displays: + displays = _parse_drm_sysfs() + + # Method 3: Enrich from debugfs — fill missing refresh rates + PSR status + if displays: + _enrich_from_debugfs(displays) + + return displays + + +def _parse_xrandr(output: str) -> list[DisplayInfo]: + """Parse xrandr --current output for connected displays. + + Example output: + eDP-1 connected primary 2256x1504+0+0 (...) 285mm x 190mm + 2256x1504 59.99*+ + 1920x1280 59.99 + DP-1 connected 3840x2160+2256+0 (...) 600mm x 340mm + 3840x2160 60.00*+ 30.00 + 1920x1080 60.00 30.00 + HDMI-A-1 connected (normal ...) + 3840x2160 60.00 + 30.00 + + The '*' marks the current active mode. + The '+' marks the preferred mode (fallback if no active mode). + """ + displays = [] + current_display = None + found_active = False # Whether we found a '*' mode for current display + preferred_res = "" # First '+' mode as fallback + preferred_rate = "" + + for line in output.split('\n'): + # Connector line: "eDP-1 connected primary 2256x1504+0+0 ..." + conn_match = re.match( + r'^(\S+)\s+connected\s+(?:primary\s+)?(?:(\d+x\d+)\+\d+\+\d+)?', + line + ) + if conn_match: + # Save previous display + if current_display: + if not found_active and preferred_res: + # No active mode found — use preferred + current_display.resolution = preferred_res + current_display.refresh_rate = preferred_rate + if current_display.resolution: + displays.append(current_display) + + connector = conn_match.group(1) + current_display = DisplayInfo( + connector=connector, + is_internal=connector.lower().startswith('edp'), + ) + # Resolution from the connector line geometry (active output) + if conn_match.group(2): + current_display.resolution = conn_match.group(2) + found_active = False + preferred_res = "" + preferred_rate = "" + continue + + # Skip disconnected connectors + if re.match(r'^(\S+)\s+disconnected', line): + # Save previous display before moving on + if current_display: + if not found_active and preferred_res: + current_display.resolution = preferred_res + current_display.refresh_rate = preferred_rate + if current_display.resolution: + displays.append(current_display) + current_display = None + found_active = False + continue + + if not current_display: + continue + + # Mode line: " 2256x1504 59.99*+ 48.00" + # Active mode (has '*') + if '*' in line and not found_active: + mode_match = re.match(r'^\s+(\d+x\d+)\s+([\d.]+)\*', line) + if mode_match: + current_display.resolution = mode_match.group(1) + current_display.refresh_rate = mode_match.group(2) + found_active = True + + # Preferred mode (has '+') — capture as fallback + if '+' in line and not preferred_res: + pref_match = re.match(r'^\s+(\d+x\d+)\s+([\d.]+)', line) + if pref_match: + preferred_res = pref_match.group(1) + preferred_rate = pref_match.group(2) + + # Don't forget the last one + if current_display: + if not found_active and preferred_res: + current_display.resolution = preferred_res + current_display.refresh_rate = preferred_rate + if current_display.resolution: + displays.append(current_display) + + return displays + + +def _parse_drm_sysfs() -> list[DisplayInfo]: + """Fall back to sysfs DRM connector info. + + Reads /sys/class/drm/card*-*/ directories for: + - status: "connected" or "disconnected" + - modes: list of supported modes (first = preferred) + + This gives preferred mode, not necessarily the active mode, + but it's the best we can do without a display server connection. + """ + displays = [] + drm_path = Path('/sys/class/drm') + + if not drm_path.exists(): + return displays + + for connector_dir in sorted(drm_path.iterdir()): + # Match card*-ConnectorName-N (e.g., card1-eDP-1, card1-DP-1) + dir_match = re.match(r'^card\d+-(.+)$', connector_dir.name) + if not dir_match: + continue + + connector_name = dir_match.group(1) + + # Check if connected + status_file = connector_dir / 'status' + try: + if not status_file.exists(): + continue + status = status_file.read_text().strip() + if status != 'connected': + continue + except (OSError, PermissionError): + continue + + display = DisplayInfo( + connector=connector_name, + is_internal=connector_name.lower().startswith('edp'), + ) + + # Read preferred mode from modes file (first line) + modes_file = connector_dir / 'modes' + try: + if modes_file.exists(): + modes = modes_file.read_text().strip().split('\n') + if modes and modes[0]: + # Format: "2256x1504" or "2560x1600" (no refresh in sysfs modes) + display.resolution = modes[0].strip() + except (OSError, PermissionError): + pass + + if display.resolution: + displays.append(display) + + return displays + + +def detect_all_hardware() -> HardwareInfo: + """Detect all hardware information.""" + hw = HardwareInfo() + + hw.gpu = detect_gpus() + hw.nvme = detect_nvme_devices() + + # Pass already-detected NVMe models to avoid duplicate sudo nvme id-ctrl calls + nvme_models = {nvme.device: nvme.model for nvme in hw.nvme if nvme.model} + hw.disk_health = check_disk_health(known_nvme_models=nvme_models) + hw.wifi = detect_wifi() + hw.ram = detect_ram() + hw.rfkill_devices = detect_rfkill() + hw.framework = detect_framework_info() + hw.webcam = detect_webcam(is_framework=hw.framework.is_framework) + hw.displays = detect_displays() + hw.cpu_vendor, hw.cpu_model, hw.amd_generation = detect_cpu_info() + + return hw + + +def format_hardware_report(hw: HardwareInfo) -> list[str]: + """Format hardware info for the diagnostic report.""" + lines = [] + + lines.append("Hardware Context:") + + # GPU + for gpu in hw.gpu: + gpu_type = "dGPU" if gpu.is_discrete else "iGPU" + driver_str = f" [{gpu.driver}]" if gpu.driver else " [no driver loaded]" + lines.append(f" GPU ({gpu_type}): {gpu.name}{driver_str}") + + # Storage + if hw.nvme: + lines.append(" Storage:") + for nvme in hw.nvme: + lines.append(f" {Path(nvme.device).name}: {nvme.model}") + + # WiFi + if hw.wifi: + lines.append(f" WiFi: {hw.wifi.name}") + + # RAM + if hw.ram: + ram_str = f" RAM: {hw.ram.total_gb} GB" + if hw.ram.ram_type: + ram_str += f" {hw.ram.ram_type}" + if hw.ram.speed_mhz: + ram_str += f" @ {hw.ram.speed_mhz} MHz" + lines.append(ram_str) + + # RF Kill Status (wireless device blocking) + if hw.rfkill_devices: + blocked_devices = [d for d in hw.rfkill_devices if d.soft_blocked or d.hard_blocked] + if blocked_devices: + lines.append(" RF Kill:") + for dev in blocked_devices: + status_parts = [] + if dev.hard_blocked: + status_parts.append("❌ hardware blocked") + if dev.soft_blocked: + status_parts.append("⚠️ software blocked") + lines.append(f" {dev.device_type} ({dev.name}): {', '.join(status_parts)}") + + # Webcam & Mic + if hw.webcam: + cam = hw.webcam + if cam.detected: + name = cam.usb_name or cam.device_name or 'Detected' + id_str = f" ({cam.usb_id})" if cam.usb_id else "" + dev_str = f", {cam.v4l_devices[0]}" if cam.v4l_devices else "" + lines.append(f" Webcam: {name}{id_str}{dev_str}") + else: + lines.append(" Webcam: ❌ Not detected") + + if not cam.mic_capture_devices: + lines.append(" Mic: ⚠️ No ALSA capture devices found") + + # Displays + if hw.displays: + for disp in hw.displays: + label = "Internal" if disp.is_internal else "External" + res_str = disp.resolution or "unknown" + if disp.refresh_rate: + res_str += f" @ {disp.refresh_rate} Hz" + if disp.psr_status: + res_str += f" [PSR: {disp.psr_status}]" + lines.append(f" Display ({label}): {disp.connector} — {res_str}") + else: + lines.append(" Display: (no connected displays detected)") + + return lines + + +def format_disk_health_report(hw: HardwareInfo) -> list[str]: + """Format disk health info for the diagnostic report.""" + lines = [] + + if not hw.disk_health: + return lines + + lines.append("Disk Health:") + + for disk in hw.disk_health: + device_name = Path(disk.device).name + status_icon = "✅" if disk.healthy else "⚠️" + + lines.append(f" {device_name}: {disk.model}") + lines.append(f" Status: {status_icon} {disk.health_status}") + + # NVMe specific - show actual useful stats + if disk.is_nvme: + # Usage stats + usage_parts = [] + if disk.data_written_tb is not None: + usage_parts.append(f"{disk.data_written_tb} TB written") + if disk.power_on_hours is not None: + days = disk.power_on_hours // 24 + usage_parts.append(f"{disk.power_on_hours:,} hours ({days} days)") + if disk.power_cycles is not None: + usage_parts.append(f"{disk.power_cycles:,} power cycles") + if usage_parts: + lines.append(f" Usage: {', '.join(usage_parts)}") + + # Health indicators + health_parts = [] + if disk.media_errors is not None: + if disk.media_errors == 0: + health_parts.append("✅ No media errors") + else: + health_parts.append(f"❌ {disk.media_errors} media errors") + if disk.unsafe_shutdowns is not None and disk.unsafe_shutdowns > 0: + # Only show if notably high relative to power cycles + if disk.power_cycles and disk.unsafe_shutdowns > disk.power_cycles * 0.5: + health_parts.append(f"⚠️ {disk.unsafe_shutdowns} unexpected power-offs") + # Otherwise just informational, no warning icon + if health_parts: + lines.append(f" Health: {', '.join(health_parts)}") + + # Endurance (only show if getting worn) + if disk.percentage_used is not None and disk.percentage_used > 0: + wear_icon = "✅" if disk.percentage_used < 80 else "⚠️" if disk.percentage_used < 90 else "❌" + lines.append(f" Endurance: {wear_icon} {disk.percentage_used}% of rated lifespan used") + + # SATA specific + else: + if disk.reallocated_sectors is not None and disk.reallocated_sectors > 0: + lines.append(f" ⚠️ Reallocated sectors: {disk.reallocated_sectors}") + if disk.pending_sectors is not None and disk.pending_sectors > 0: + lines.append(f" ⚠️ Pending sectors: {disk.pending_sectors}") + + # Temperature + if disk.temperature is not None: + # NVMe drives run warm - typically fine up to 70°C + temp_icon = "✅" if disk.temperature < 55 else "⚠️" if disk.temperature < 70 else "❌" + lines.append(f" Temp: {temp_icon} {disk.temperature}°C") + + # Additional warnings + for warning in disk.warnings: + if warning not in str(lines): # Avoid duplicates + lines.append(f" ⚠️ {warning}") + + return lines diff --git a/fw-log-tool/framework_diagnostic/log_summary.py b/fw-log-tool/framework_diagnostic/log_summary.py new file mode 100644 index 0000000..63d0477 --- /dev/null +++ b/fw-log-tool/framework_diagnostic/log_summary.py @@ -0,0 +1,461 @@ +"""Log summary module - extracts system activity from raw logs. + +Scans combined journalctl + dmesg content and produces a structured +summary of system lifecycle events, suspend/resume cycles, and +critical error detection. Raw logs are always preserved unmodified. +""" +import re +import os +from dataclasses import dataclass, field +from typing import List, Optional +from pathlib import Path + +# ── Critical event patterns ────────────────────────────────────────── +# Checked against combined log text. Both journalctl and dmesg formats. +# No kernel: prefix requirement — works with either source. +_CRITICAL_CHECKS = [ + # (pattern, label, confidence) + # 'high' = exact kernel string, no known false positives + # 'medium' = correct subsystem, format may vary by kernel version + # 'low' = best-effort, may have false positives or miss variants + (r'Kernel panic\s*-\s*not syncing', 'Kernel panic', 'high'), + (r'Oops:\s', 'Kernel oops', 'high'), + (r'(?:amdgpu.*\*ERROR\*.*timeout|amdgpu.*GPU reset|amdgpu.*GPU recovery' + r'|i915.*GPU HANG|i915.*wedged|i915.*Resetting chip' + r'|\bxe\b.*\*ERROR\*.*timeout' + r'|NVRM.*Xid|nvidia-modeset.*ERROR|NVRM.*GPU has fallen off the bus)', + 'GPU error', 'medium'), + (r'nvme\s+nvme\d+:\s+I/O.*(?:error|timeout)|I/O error.*dev nvme', 'NVMe I/O error', 'high'), + (r'EXT4-fs error|BTRFS.*error.*device', 'Filesystem error', 'high'), + (r'iwlwifi.*(?:firmware error|Microcode SW error)' + r'|mt792[12]e?.*(?:firmware error|timeout)', + 'WiFi firmware crash', 'medium'), + (r'Out of memory:\s+Kill', 'OOM kill', 'high'), + (r'CPU\d+.*temperature above threshold' + r'|k10temp.*critical' + r'|thermal_zone\d+.*critical', + 'Thermal throttling', 'medium'), + # Two formats: "PM: Device X failed to suspend/resume" (standard) + # and "usb X-X: PM: failed to resume async" (xHCI/USB) + (r'PM:\s+Device\s+\S+\s+failed to (?:suspend|resume)' + r'|PM:\s+failed to (?:suspend|resume)', + 'Suspend device failure', 'high'), + (r'amd_pmc.*(?:timeout|failed)', 'AMD PMC error', 'medium'), + (r'cros_ec.*timeout', 'EC timeout', 'low'), + (r'usb\s+\S+:\s+Failed to suspend', 'USB suspend failure', 'high'), + (r'xhci_hcd.*HC died', 'xHCI controller died', 'high'), + (r'mce:\s.*Hardware Error' + r'|Machine check events logged' + r'|MCE:\s(?!In-kernel MCE decoding)', + 'Machine check exception', 'medium'), + (r'EDAC\s+(?:MC|sbridge|skx|ie31200|amd64)\d*:\s.*(?:CE|UE|[Ee]rror)', + 'Memory hardware error', 'medium'), + (r'fwupd\[\d+\].*(?:failed to (?:update|flash|write|install) firmware' + r'|Update Error:)' + r'|fwupdmgr.*failed to update', + 'fwupd update failure', 'high'), + (r'ACPI Error:' + r'|ACPI Exception:' + r'|ACPI BIOS Error', + 'ACPI error', 'low'), +] + +# ── Lifecycle event patterns ───────────────────────────────────────── +_LIFECYCLE = [ + (r'PM:\s+suspend entry', 'SUSPEND'), + (r'PM:\s+suspend exit', 'RESUME'), + (r'Linux version \S+', 'BOOT'), + (r'systemd\[1\]:\s+Shutting down', 'SHUTDOWN'), + (r'systemd\[1\]:\s+Started.*plymouth-reboot', 'REBOOT'), + (r'systemd-shutdown\[1\]:\s+Rebooting', 'REBOOT'), + (r'systemd\[1\]:\s+([\w@.-]+\.service):\s+Failed with result', 'SVC_FAIL'), + (r'systemd\[1\]:\s+Startup finished in.*=\s*(.+)', 'BOOT_DONE'), +] + +# ── Per-cycle error patterns ───────────────────────────────────────── +# Matched between suspend entry and resume exit to count cycle errors. +_CYCLE_ERROR = re.compile( + r'failed.*(?:resume|suspend)' + r'|HC died' + r'|\*ERROR\*' + r'|Call Trace' + r'|GPU (?:HANG|reset|recovery)' + r'|firmware error' + r'|I/O error', + re.IGNORECASE +) + +# Path to the xHCI resume workaround script +_XHCI_FIX_PATH = Path('/usr/local/bin/xhci-resume-fix.sh') +_XHCI_FIX_SERVICE = 'xhci-resume-fix.service' + + +@dataclass +class SuspendCycle: + """One suspend/resume cycle.""" + cycle_num: int + suspend_time: str = '' + resume_time: str = '' + errors: int = 0 + error_lines: list = field(default_factory=list) + resumed: bool = False + + +@dataclass +class SystemActivity: + """Structured summary of system events from logs.""" + boot_count: int = 0 + shutdown_count: int = 0 + reboot_count: int = 0 + boot_times: list = field(default_factory=list) + service_failures: dict = field(default_factory=dict) + suspend_cycles: list = field(default_factory=list) + critical_found: list = field(default_factory=list) + critical_clear: list = field(default_factory=list) + crashes: int = 0 + total_suspend_since_boot: int = 0 # from /sys/power/suspend_stats/success + xhci_hc_died: bool = False + xhci_fix_installed: bool = False + xhci_fix_service_enabled: bool = False + + +def _extract_timestamp(line: str) -> str: + """Pull timestamp from journalctl or dmesg format.""" + # journalctl: "Feb 18 16:58:46 hostname ..." + m = re.match(r'^(\w{3}\s+\d{1,2}\s+\d{2}:\d{2}:\d{2})', line) + if m: + return m.group(1) + # dmesg -T: "[Sun Feb 22 16:55:27 2026] ..." + m = re.match(r'^\[(\w{3}\s+\w{3}\s+\d{1,2}\s+\d{2}:\d{2}:\d{2}\s+\d{4})\]', line) + if m: + return m.group(1) + return '' + + +def _check_xhci_workaround() -> tuple: + """Check if the xHCI resume fix workaround is installed. + + Returns (script_exists: bool, service_enabled: bool) + """ + script_exists = _XHCI_FIX_PATH.is_file() + + service_enabled = False + if script_exists: + try: + import subprocess + result = subprocess.run( + ['systemctl', 'is-enabled', _XHCI_FIX_SERVICE], + capture_output=True, text=True, timeout=5 + ) + service_enabled = result.stdout.strip() == 'enabled' + except Exception: + pass + + return script_exists, service_enabled + + +def extract_activity(log_text: str) -> SystemActivity: + """Scan log text and extract structured system activity. + + Does NOT modify or filter the log text. Read-only scan. + """ + activity = SystemActivity() + lines = log_text.splitlines() + + # ── Critical checks ────────────────────────────────────────── + for pattern, label, confidence in _CRITICAL_CHECKS: + regex = re.compile(pattern) + found = False + for line in lines: + if regex.search(line): + found = True + if label == 'xHCI controller died': + activity.xhci_hc_died = True + break + if found: + activity.critical_found.append((label, confidence)) + else: + activity.critical_clear.append((label, confidence)) + + # ── Check xHCI workaround if xHCI issues detected ──────────── + found_labels = [label for label, _ in activity.critical_found] + if activity.xhci_hc_died or 'Suspend device failure' in found_labels: + script_ok, service_ok = _check_xhci_workaround() + activity.xhci_fix_installed = script_ok + activity.xhci_fix_service_enabled = service_ok + + # ── Lifecycle events (ordered) ─────────────────────────────── + events = [] # (line_num, timestamp, event_type, detail) + for i, line in enumerate(lines): + for pattern, event_type in _LIFECYCLE: + m = re.search(pattern, line) + if m: + ts = _extract_timestamp(line) + detail = '' + if event_type == 'SVC_FAIL': + detail = m.group(1) + elif event_type == 'BOOT_DONE': + detail = m.group(1).strip() + elif event_type == 'BOOT': + km = re.search(r'Linux version (\S+)', line) + if km: + detail = km.group(1) + events.append((i, ts, event_type, detail)) + break + + # ── Count lifecycle events ─────────────────────────────────── + prev_event = None # track what preceded each boot + for _, ts, etype, detail in events: + if etype == 'BOOT': + activity.boot_count += 1 + # Flush any pending boot that never got a BOOT_DONE + pending = getattr(activity, '_pending_boot', None) + if pending: + boot_ts, boot_type = pending + activity.boot_times.append((boot_ts, None, boot_type)) + # Determine boot type based on what preceded it + if prev_event == 'REBOOT': + boot_type = 'Restart' + elif prev_event == 'SHUTDOWN': + boot_type = 'Power on' + elif prev_event == 'BOOT': + boot_type = 'Crash recovery' + else: + boot_type = 'Power on' # first boot in window + # Stash for pairing with BOOT_DONE + activity._pending_boot = (ts, boot_type) + elif etype == 'SHUTDOWN': + if prev_event != 'REBOOT': # don't count shutdown that's part of a reboot + activity.shutdown_count += 1 + elif etype == 'REBOOT': + if prev_event != 'REBOOT': # dedup: plymouth + systemd-shutdown both fire + activity.reboot_count += 1 + elif etype == 'BOOT_DONE': + pending = getattr(activity, '_pending_boot', None) + if pending: + boot_ts, boot_type = pending + activity.boot_times.append((boot_ts, detail, boot_type)) + activity._pending_boot = None + else: + activity.boot_times.append((ts, detail, 'Power on')) + elif etype == 'SVC_FAIL': + activity.service_failures[detail] = activity.service_failures.get(detail, 0) + 1 + if etype in ('BOOT', 'SHUTDOWN', 'REBOOT'): + # REBOOT takes priority: systemd fires both REBOOT and SHUTDOWN + # on reboot, so don't let SHUTDOWN overwrite a preceding REBOOT + if etype == 'SHUTDOWN' and prev_event == 'REBOOT': + pass # keep REBOOT as prev_event + else: + prev_event = etype + + # Flush last pending boot if no BOOT_DONE followed + pending = getattr(activity, '_pending_boot', None) + if pending: + boot_ts, boot_type = pending + activity.boot_times.append((boot_ts, None, boot_type)) + activity._pending_boot = None + + # ── Crash detection ────────────────────────────────────────── + # Boot without preceding shutdown or reboot = crash/power loss + prev_was_down = True # first boot is normal + for _, _, etype, _ in events: + if etype in ('SHUTDOWN', 'REBOOT'): + prev_was_down = True + elif etype == 'BOOT': + if not prev_was_down: + activity.crashes += 1 + prev_was_down = False + + # ── Suspend/resume scoreboard ──────────────────────────────── + # Pair suspend entries with resume exits, count errors between them + suspend_indices = [] + resume_indices = [] + for idx, (line_num, ts, etype, _) in enumerate(events): + if etype == 'SUSPEND': + suspend_indices.append((line_num, ts)) + elif etype == 'RESUME': + resume_indices.append((line_num, ts)) + + cycle_num = 0 + ri = 0 # resume index pointer + for s_line, s_ts in suspend_indices: + cycle_num += 1 + cycle = SuspendCycle(cycle_num=cycle_num, suspend_time=s_ts) + + # Find the next resume after this suspend + r_line = None + r_ts = '' + while ri < len(resume_indices): + if resume_indices[ri][0] > s_line: + r_line = resume_indices[ri][0] + r_ts = resume_indices[ri][1] + ri += 1 + break + ri += 1 + + if r_line is not None: + cycle.resumed = True + cycle.resume_time = r_ts + # Capture errors between suspend and resume + for line in lines[s_line:r_line + 1]: + if _CYCLE_ERROR.search(line): + cycle.errors += 1 + # Strip timestamp, keep the message + stripped = re.sub( + r'^(?:\w{3}\s+\d{1,2}\s+\d{2}:\d{2}:\d{2}\s+\S+\s+|' + r'\[.*?\]\s*)', '', line).strip() + if stripped: + cycle.error_lines.append(stripped) + else: + cycle.resumed = False + + activity.suspend_cycles.append(cycle) + + # ── Kernel suspend total (authoritative, not limited by log window) ── + try: + success_path = Path('/sys/power/suspend_stats/success') + if success_path.exists(): + activity.total_suspend_since_boot = int(success_path.read_text().strip()) + except (ValueError, OSError): + pass + + return activity + + +def format_activity(activity: SystemActivity, time_range: str = "") -> list: + """Format SystemActivity into report lines.""" + sections = [] + sections.append('=' * 60) + sections.append('SYSTEM ACTIVITY') + sections.append('=' * 60) + if time_range: + sections.append(f' Log period: {time_range}') + sections.append('') + + # ── Event summary ──────────────────────────────────────── + event_parts = [] + if activity.boot_count > 0: + event_parts.append(f'{activity.boot_count} boot(s)') + if activity.reboot_count > 0: + event_parts.append(f'{activity.reboot_count} reboot(s)') + if activity.shutdown_count > 0: + event_parts.append(f'{activity.shutdown_count} shutdown(s)') + if activity.suspend_cycles: + in_window = len(activity.suspend_cycles) + kernel_total = activity.total_suspend_since_boot + if kernel_total > 0: + event_parts.append(f'{in_window} sleep/wake cycle(s) in log window ({kernel_total} total since boot)') + else: + event_parts.append(f'{in_window} sleep/wake cycle(s)') + + if event_parts: + sections.append(f' Events: {", ".join(event_parts)}') + else: + sections.append(' Events: none detected') + + # Boot times (from log window — shows what each boot was, when, and how long) + for ts, duration, boot_type in activity.boot_times: + if ts and duration: + sections.append(f' {boot_type} at {ts} — startup took {duration}') + elif ts: + sections.append(f' {boot_type} at {ts} — no startup duration (interrupted)') + elif duration: + sections.append(f' {boot_type} — startup took {duration}') + else: + sections.append(f' {boot_type}') + + # Crashes + if activity.crashes > 0: + sections.append(f' ⚠️ Crashes detected: {activity.crashes} ' + f'(boot without preceding shutdown)') + + # Service failures + if activity.service_failures: + sections.append('') + sections.append(' Service failures:') + for svc, count in sorted(activity.service_failures.items(), + key=lambda x: -x[1]): + sections.append(f' {svc} ({count}x)') + + # ── Suspend/resume detail ──────────────────────────────────── + if activity.suspend_cycles: + sections.append('') + total = len(activity.suspend_cycles) + clean = sum(1 for c in activity.suspend_cycles + if c.resumed and c.errors == 0) + with_errors = sum(1 for c in activity.suspend_cycles + if c.resumed and c.errors > 0) + didnt_resume = sum(1 for c in activity.suspend_cycles + if not c.resumed) + + kernel_total = activity.total_suspend_since_boot + if kernel_total > 0 and kernel_total != total: + sections.append(f' Suspend/Resume: {total} of {kernel_total} sleep/wake cycle(s) in log window') + else: + sections.append(f' Suspend/Resume: {total} sleep/wake cycle(s) detected') + sections.append('') + + # Per-cycle detail + for c in activity.suspend_cycles: + s_time = c.suspend_time if c.suspend_time else 'unknown time' + if c.resumed and c.errors == 0: + r_time = c.resume_time if c.resume_time else 'unknown time' + sections.append( + f' Cycle {c.cycle_num}: Slept at {s_time} → ' + f'Woke at {r_time} — ✅ clean (no errors)') + elif c.resumed and c.errors > 0: + r_time = c.resume_time if c.resume_time else 'unknown time' + sections.append( + f' Cycle {c.cycle_num}: Slept at {s_time} → ' + f'Woke at {r_time} — ⚠️ {c.errors} error(s) during wake:') + for err in c.error_lines: + sections.append(f' → {err}') + else: + sections.append( + f' Cycle {c.cycle_num}: Slept at {s_time} → ' + f'❌ Never woke up (no resume found in logs)') + + # Summary + sections.append('') + summary_parts = [] + if clean: + summary_parts.append(f'{clean} clean') + if with_errors: + summary_parts.append(f'{with_errors} woke with errors') + if didnt_resume: + summary_parts.append(f'{didnt_resume} failed to wake') + sections.append(f' Summary: {", ".join(summary_parts)}') + + # xHCI workaround status (only when relevant) + if activity.xhci_hc_died: + sections.append('') + if activity.xhci_fix_installed and activity.xhci_fix_service_enabled: + sections.append(' ℹ️ xHCI resume workaround: ✅ installed and enabled') + elif activity.xhci_fix_installed: + sections.append(' ℹ️ xHCI resume workaround: ⚠️ script exists but service not enabled') + sections.append(f' Run: sudo systemctl enable {_XHCI_FIX_SERVICE}') + else: + sections.append(' ℹ️ xHCI resume workaround: ❌ not installed') + sections.append(' xHCI controller dies on every resume (HC died)') + sections.append(' Workaround: install xhci-resume-fix.sh + systemd service') + sections.append(' (rebinds xHCI controller after resume, upstream kernel fix pending)') + + elif activity.total_suspend_since_boot > 0: + # Kernel reports suspend cycles but none fell in the log window + sections.append('') + sections.append(f' Suspend/Resume: {activity.total_suspend_since_boot} sleep/wake cycle(s) since boot (none in log window)') + + # ── Critical checks ────────────────────────────────────────── + sections.append('') + if activity.critical_clear: + clear_labels = [label for label, _ in activity.critical_clear] + sections.append(' ✅ Clear: ' + ', '.join(clear_labels)) + if activity.critical_found: + sections.append(' Detected:') + for label, conf in activity.critical_found: + sections.append(f' ❌ {label} [{conf} confidence]') + else: + sections.append(' No critical errors detected.') + sections.append(' ⚠️ Pattern-matched only — not a comprehensive scan. Review raw logs for issues not listed above.') + + return sections diff --git a/fw-log-tool/framework_diagnostic/network.py b/fw-log-tool/framework_diagnostic/network.py new file mode 100644 index 0000000..37fa968 --- /dev/null +++ b/fw-log-tool/framework_diagnostic/network.py @@ -0,0 +1,462 @@ +""" +Network connectivity checking. + +Detects: internet connectivity, WiFi/Ethernet status, IP addresses, +DNS servers, VPN connections (OpenVPN, WireGuard, NetworkManager VPNs), +and WiFi power save state. +""" + +import re +from dataclasses import dataclass, field +from pathlib import Path +from typing import Optional + +from .utils import run_command + + +def _get_wifi_interface() -> str: + """Get the primary wireless interface name from iw dev. + + Returns interface name (e.g. 'wlan0', 'wlp1s0') or empty string. + """ + rc, stdout, _ = run_command(['iw', 'dev']) + if rc != 0: + return "" + + # iw dev output: + # phy#0 + # Interface wlan0 + for line in stdout.split('\n'): + stripped = line.strip() + if stripped.startswith('Interface '): + return stripped.split()[1] + return "" + + +def _check_wifi_power_save(interface: str) -> Optional[bool]: + """Check WiFi power save state via iw. + + Returns True if on, False if off, None if unknown. + """ + if not interface: + return None + + rc, stdout, _ = run_command(['iw', 'dev', interface, 'get', 'power_save']) + if rc != 0: + return None + + # Output: "Power save: on" or "Power save: off" + output = stdout.strip().lower() + if 'power save: on' in output: + return True + elif 'power save: off' in output: + return False + return None + + +# ── IP address detection ───────────────────────────────────────────── + +@dataclass +class InterfaceAddress: + """One IP address on one interface.""" + interface: str # e.g. "wlan0", "enp1s0" + address: str # e.g. "192.168.1.100/24", "fe80::1/64" + family: str # "ipv4" or "ipv6" + + +def _detect_ip_addresses() -> list: + """Get all IP addresses on all interfaces via 'ip addr show'. + + Uses 'ip' from iproute2 (present on every modern Linux distro). + Skips loopback (lo). + Returns list of InterfaceAddress. + """ + results = [] + + rc, stdout, _ = run_command(['ip', '-o', 'addr', 'show']) + if rc != 0: + return results + + # -o gives one-line-per-address output: + # 2: enp1s0 inet 192.168.1.100/24 brd 192.168.1.255 scope global enp1s0 + # 2: enp1s0 inet6 fe80::1/64 scope link + for line in stdout.splitlines(): + parts = line.split() + if len(parts) < 4: + continue + + iface = parts[1].rstrip(':') + if iface == 'lo': + continue + + proto = parts[2] # "inet" or "inet6" + addr = parts[3] # e.g. "192.168.1.100/24" + + if proto == 'inet': + results.append(InterfaceAddress(iface, addr, 'ipv4')) + elif proto == 'inet6': + # Skip link-local (fe80::) — noise for diagnostics + if addr.startswith('fe80:'): + continue + results.append(InterfaceAddress(iface, addr, 'ipv6')) + + return results + + +# ── DNS detection ──────────────────────────────────────────────────── + +def _detect_dns_servers() -> list: + """Detect configured DNS servers. + + Strategy (in order): + 1. resolvectl status — systemd-resolved (Ubuntu, Fedora, Arch default) + 2. nmcli dev show — NetworkManager (fallback) + 3. /etc/resolv.conf — universal last resort + + Returns list of DNS server address strings (deduped, order preserved). + """ + servers = [] + seen = set() + + def _add(addr: str): + addr = addr.strip() + if addr and addr not in seen: + seen.add(addr) + servers.append(addr) + + # Method 1: resolvectl (systemd-resolved) + rc, stdout, _ = run_command(['resolvectl', 'status'], timeout=5) + if rc == 0: + # Lines like: "DNS Servers: 8.8.8.8 1.1.1.1" + # or: "DNS Servers: 8.8.8.8" + # or: "Current DNS Server: 8.8.8.8" + for line in stdout.splitlines(): + stripped = line.strip() + if stripped.startswith('DNS Servers:') or stripped.startswith('Current DNS Server:'): + parts = stripped.split(':', 1)[1].strip().split() + for p in parts: + _add(p) + if servers: + return servers + + # Method 2: nmcli (NetworkManager) + rc, stdout, _ = run_command( + ['nmcli', '-t', '-f', 'IP4.DNS,IP6.DNS', 'dev', 'show'], timeout=5 + ) + if rc == 0: + for line in stdout.splitlines(): + # Format: IP4.DNS[1]:8.8.8.8 + if line.startswith('IP4.DNS') or line.startswith('IP6.DNS'): + parts = line.split(':', 1) + if len(parts) == 2: + _add(parts[1]) + if servers: + return servers + + # Method 3: /etc/resolv.conf (universal) + resolv = Path('/etc/resolv.conf') + if resolv.exists(): + try: + for line in resolv.read_text().splitlines(): + line = line.strip() + if line.startswith('nameserver '): + parts = line.split() + if len(parts) >= 2: + _add(parts[1]) + except Exception: + pass + + return servers + + +# ── VPN detection ──────────────────────────────────────────────────── + +@dataclass +class VPNConnection: + """A detected VPN connection.""" + name: str # connection name or interface name + vpn_type: str # "wireguard", "openvpn", "vpn" (generic NM VPN) + interface: str # e.g. "wg0", "tun0", "" + active: bool # True if currently up + + +def _detect_vpn_connections() -> list: + """Detect active VPN connections. + + Detection methods (all run, results deduped by interface): + 1. WireGuard: 'ip link show type wireguard' (kernel-native, no extra tools) + 2. OpenVPN: tun/tap interfaces + process detection via 'pgrep -a openvpn' + 3. NetworkManager: 'nmcli -t connection show --active' filtered by type + + Does NOT require wireguard-tools or openvpn packages — detection only. + Returns list of VPNConnection. + """ + vpns = [] + seen_interfaces = set() + + # ── WireGuard (kernel interface type) ──────────────────────── + rc, stdout, _ = run_command(['ip', 'link', 'show', 'type', 'wireguard']) + if rc == 0 and stdout.strip(): + # Output like: "4: wg0: mtu 1420 ..." + for line in stdout.splitlines(): + m = re.match(r'^\d+:\s+(\S+):', line) + if m: + iface = m.group(1) + if iface not in seen_interfaces: + seen_interfaces.add(iface) + vpns.append(VPNConnection( + name=iface, + vpn_type='wireguard', + interface=iface, + active=True, + )) + + # ── OpenVPN (tun/tap + process) ────────────────────────────── + # First check if openvpn is running at all (avoids false positives + # from tun/tap interfaces used by other software) + rc_pgrep, pgrep_out, _ = run_command(['pgrep', '-a', 'openvpn'], timeout=3) + openvpn_running = (rc_pgrep == 0 and 'openvpn' in pgrep_out) + + if openvpn_running: + rc, stdout, _ = run_command(['ip', '-o', 'link', 'show']) + if rc == 0: + for line in stdout.splitlines(): + parts = line.split() + if len(parts) < 2: + continue + iface = parts[1].rstrip(':') + # tun0, tun1, tap0, tap1 etc — common OpenVPN interfaces + if re.match(r'^(tun|tap)\d+$', iface) and iface not in seen_interfaces: + seen_interfaces.add(iface) + vpns.append(VPNConnection( + name=f'OpenVPN ({iface})', + vpn_type='openvpn', + interface=iface, + active=True, + )) + + # ── NetworkManager VPNs ────────────────────────────────────── + # Catches NM-managed VPNs: OpenConnect, VPNC, PPTP, L2TP, + # and NM-managed WireGuard/OpenVPN that might not show via + # the kernel-level checks above. + rc, stdout, _ = run_command( + ['nmcli', '-t', '-f', 'NAME,TYPE,DEVICE', 'connection', 'show', '--active'], + timeout=5, + ) + if rc == 0: + for line in stdout.splitlines(): + # Format: "My VPN:vpn:tun0" or "wg-tunnel:wireguard:wg0" + parts = line.split(':') + if len(parts) >= 2: + conn_name = parts[0] + conn_type = parts[1] + conn_dev = parts[2] if len(parts) >= 3 else '' + + if conn_type in ('vpn', 'wireguard'): + if conn_dev and conn_dev in seen_interfaces: + continue # already found via kernel detection + if conn_dev: + seen_interfaces.add(conn_dev) + # Map NM type to our type + if conn_type == 'wireguard': + vtype = 'wireguard' + else: + vtype = 'vpn' + vpns.append(VPNConnection( + name=conn_name, + vpn_type=vtype, + interface=conn_dev, + active=True, + )) + + return vpns + + +# ── Main dataclass and orchestration ───────────────────────────────── + +@dataclass +class NetworkStatus: + """Network connectivity status.""" + internet_working: bool = False + wifi_connected: bool = False + wifi_ssid: Optional[str] = None + ethernet_connected: bool = False + + interfaces_up: int = 0 + + # WiFi power save + wifi_interface: str = "" # e.g. "wlan0", "wlp1s0" + wifi_power_save: Optional[bool] = None # True=on, False=off, None=unknown + wifi_power_save_service: Optional[bool] = None # True=service exists + + # IP addresses + ip_addresses: list = field(default_factory=list) # list[InterfaceAddress] + + # DNS + dns_servers: list = field(default_factory=list) # list[str] + + # VPN + vpn_connections: list = field(default_factory=list) # list[VPNConnection] + + +def ping_test(host: str, timeout: int = 2) -> bool: + """Test connectivity by pinging a host.""" + rc, _, _ = run_command(['ping', '-c', '1', '-W', str(timeout), host], timeout=timeout+1) + return rc == 0 + + +def check_internet_connectivity() -> bool: + """Check if internet is working by pinging known reliable hosts.""" + # Try Google DNS + if ping_test('8.8.8.8'): + return True + + # Try Cloudflare DNS + if ping_test('1.1.1.1'): + return True + + return False + + +def check_wifi_status() -> tuple[bool, Optional[str]]: + """ + Check WiFi connection status. + + Returns: + Tuple of (connected, ssid) + """ + # Use nmcli for WiFi status + rc, stdout, _ = run_command(['nmcli', '-t', '-f', 'ACTIVE,SSID', 'dev', 'wifi']) + if rc == 0: + for line in stdout.split('\n'): + if line.startswith('yes:'): + ssid = line.split(':', 1)[1] if ':' in line else None + return True, ssid + + return False, None + + +def check_ethernet_status() -> bool: + """Check if any Ethernet interface is up.""" + rc, stdout, _ = run_command(['ip', 'link', 'show']) + if rc != 0: + return False + + # Look for eth*, enp*, eno* interfaces in UP state + for line in stdout.split('\n'): + if re.search(r'(eth|enp|eno)\d+.*state UP', line): + return True + + return False + + +def count_interfaces_up() -> int: + """Count number of network interfaces in UP state.""" + rc, stdout, _ = run_command(['ip', 'link', 'show']) + if rc != 0: + return 0 + + count = 0 + for line in stdout.split('\n'): + if 'state UP' in line: + count += 1 + + return count + + +def check_network_connectivity() -> NetworkStatus: + """ + Check complete network connectivity status. + + Returns: + NetworkStatus object + """ + status = NetworkStatus() + + # Check interfaces + status.interfaces_up = count_interfaces_up() + + # Check internet + status.internet_working = check_internet_connectivity() + + # Check WiFi + status.wifi_connected, status.wifi_ssid = check_wifi_status() + + # Check Ethernet + status.ethernet_connected = check_ethernet_status() + + # WiFi power save + status.wifi_interface = _get_wifi_interface() + status.wifi_power_save = _check_wifi_power_save(status.wifi_interface) + status.wifi_power_save_service = Path('/etc/systemd/system/wifi-power-save.service').exists() + + # IP addresses + status.ip_addresses = _detect_ip_addresses() + + # DNS servers + status.dns_servers = _detect_dns_servers() + + # VPN connections + status.vpn_connections = _detect_vpn_connections() + + return status + + +def format_network_report(status: NetworkStatus) -> list[str]: + """Format network status for the diagnostic report.""" + lines = [] + + lines.append("Network Connectivity:") + + # Internet + if status.internet_working: + lines.append(" Internet: ✅ Connected") + else: + lines.append(" Internet: ❌ Not connected") + + # WiFi + if status.wifi_connected: + lines.append(f' WiFi: ✅ Connected to "{status.wifi_ssid}"') + else: + lines.append(" WiFi: ❌ Not connected") + + # Ethernet + if status.ethernet_connected: + lines.append(" Ethernet: ✅ Connected") + else: + lines.append(" Ethernet: ❌ Not connected") + + # VPN + if status.vpn_connections: + for vpn in status.vpn_connections: + iface_str = f' ({vpn.interface})' if vpn.interface else '' + lines.append(f' VPN: ✅ {vpn.name} [{vpn.vpn_type}]{iface_str}') + else: + lines.append(" VPN: none detected") + + # IP addresses + if status.ip_addresses: + lines.append("") + lines.append(" IP Addresses:") + for addr in status.ip_addresses: + lines.append(f' {addr.interface}: {addr.address} ({addr.family})') + else: + lines.append(" IP Addresses: none detected") + + # DNS servers + if status.dns_servers: + lines.append(f' DNS: {", ".join(status.dns_servers)}') + else: + lines.append(" DNS: none detected") + + # WiFi power save + if status.wifi_interface: + if status.wifi_power_save is True: + lines.append(f" WiFi Power Save: on ({status.wifi_interface})") + elif status.wifi_power_save is False: + lines.append(f" WiFi Power Save: off ({status.wifi_interface})") + + if status.wifi_power_save_service: + lines.append(" wifi-power-save.service: ✅ installed") + + return lines diff --git a/fw-log-tool/framework_diagnostic/output.py b/fw-log-tool/framework_diagnostic/output.py new file mode 100644 index 0000000..42d967e --- /dev/null +++ b/fw-log-tool/framework_diagnostic/output.py @@ -0,0 +1,101 @@ +""" +Output formatting, ANSI colors, and progress display. +""" + +import sys +from enum import Enum + + +class Color(Enum): + """ANSI color codes.""" + RESET = '\033[0m' + BOLD = '\033[1m' + RED = '\033[0;31m' + YELLOW = '\033[1;33m' + GREEN = '\033[0;32m' + BLUE = '\033[0;34m' + CYAN = '\033[0;36m' + MAGENTA = '\033[0;35m' + + +def colorize(text: str, color: Color, bold: bool = False) -> str: + """Apply ANSI color to text.""" + prefix = Color.BOLD.value if bold else '' + return f"{prefix}{color.value}{text}{Color.RESET.value}" + + +def print_colored(text: str, color: Color, bold: bool = False, end: str = '\n'): + """Print colored text to stdout.""" + print(colorize(text, color, bold), end=end) + + +def print_error(text: str): + """Print error message in red.""" + print_colored(f"❌ {text}", Color.RED, bold=True) + + +def print_warning(text: str): + """Print warning message in yellow.""" + print_colored(f"⚠️ {text}", Color.YELLOW) + + +def print_success(text: str): + """Print success message in green.""" + print_colored(f"✅ {text}", Color.GREEN) + + +def print_info(text: str): + """Print info message in blue.""" + print_colored(f"ℹ️ {text}", Color.BLUE) + + +def show_progress(percentage: int, context: str = "Processing"): + """Display a progress bar.""" + bar_width = 40 + filled = int(bar_width * percentage / 100) + bar = '█' * filled + '░' * (bar_width - filled) + sys.stdout.write(f"\r{Color.CYAN.value}[{bar}] {percentage:3d}% - {context}{Color.RESET.value}") + sys.stdout.flush() + if percentage >= 100: + print() # Newline when complete + + +class ReportBuilder: + """Builds the diagnostic report output.""" + + def __init__(self): + self.lines: list[str] = [] + + def add_line(self, line: str = ""): + """Add a line to the report.""" + self.lines.append(line) + + def add_section(self, title: str): + """Add a section header.""" + self.add_line() + self.add_line(f"===== {title} =====") + self.add_line() + + def add_key_value(self, key: str, value: str, indent: int = 0): + """Add a key-value pair.""" + prefix = " " * indent + self.add_line(f"{prefix}{key}: {value}") + + def add_bullet(self, text: str, indent: int = 0): + """Add a bullet point.""" + prefix = " " * indent + self.add_line(f"{prefix}• {text}") + + def add_indented(self, text: str, indent: int = 1): + """Add indented text.""" + prefix = " " * indent + self.add_line(f"{prefix}{text}") + + def get_content(self) -> str: + """Get the full report content.""" + return '\n'.join(self.lines) + + def write_to_file(self, filepath: str): + """Write report to a file.""" + with open(filepath, 'w') as f: + f.write(self.get_content()) diff --git a/fw-log-tool/framework_diagnostic/sleep.py b/fw-log-tool/framework_diagnostic/sleep.py new file mode 100644 index 0000000..cec81c3 --- /dev/null +++ b/fw-log-tool/framework_diagnostic/sleep.py @@ -0,0 +1,779 @@ +""" +Sleep/suspend/resume status analysis. + +NEW FUNCTION: check_sleep_status() +Reports: +- Sleep mode (s2idle vs deep) +- s2idle status +- ACPI states +- Suspend/resume counts +- Inhibitors +- Resume errors +- AMD PMC issues +""" + +import re +import subprocess +from dataclasses import dataclass, field +from pathlib import Path +from typing import Optional +from enum import Enum + + +class SleepMode(Enum): + """Available sleep modes.""" + S2IDLE = 's2idle' + DEEP = 'deep' + UNKNOWN = 'unknown' + + +@dataclass +class SleepStatus: + """Complete sleep/suspend status.""" + # Current configuration + current_mode: SleepMode = SleepMode.UNKNOWN + available_modes: list[str] = field(default_factory=list) + s2idle_enabled: bool = False + + # Raw evidence from /sys files + mem_sleep_raw: str = "" # Raw content of /sys/power/mem_sleep + state_raw: str = "" # Raw content of /sys/power/state + + # Kernel suspend stats (from /sys/power/suspend_stats/) + kernel_suspend_success: int = 0 + kernel_suspend_fail: int = 0 + + # ACPI states + acpi_states: list[str] = field(default_factory=list) + + # Counters from logs + suspend_count: int = 0 + resume_count: int = 0 + failed_suspend_count: int = 0 + failed_resume_count: int = 0 + + # Timestamps from logs + last_suspend_time: str = "" + last_resume_time: str = "" + + # PSR (Panel Self Refresh) status + psr_enabled: bool = False + psr_status: str = "" + psr_issues: list[str] = field(default_factory=list) # Issues like screen blinking on resume + + # Inhibitors + inhibitors: list[str] = field(default_factory=list) + + # Error tracking + resume_errors: list[str] = field(default_factory=list) + amd_pmc_issues: list[str] = field(default_factory=list) + + # Health status + is_healthy: bool = True + issues: list[str] = field(default_factory=list) + + # Sleep blockers (NEW) + blockers: list['S2IdleBlocker'] = field(default_factory=list) + + # Framework workaround services + disable_wakeup_service: Optional[bool] = None # True=exists + + +@dataclass +class S2IdleBlocker: + """A device or condition blocking proper s2idle/sleep.""" + device: str + reason: str + fix: str + source: str = "" # Where we found this (log, sysfs, etc.) + + +# Inhibitors to ignore (provide no useful info - normal system services) + + + +def get_current_sleep_mode() -> tuple[SleepMode, list[str], str]: + """ + Read the current sleep mode from /sys/power/mem_sleep. + + The file format is: "[s2idle] deep" where brackets indicate current. + + Returns: + Tuple of (current_mode, available_modes, raw_content) + """ + mem_sleep_path = Path('/sys/power/mem_sleep') + + if not mem_sleep_path.exists(): + return SleepMode.UNKNOWN, [], "" + + try: + content = mem_sleep_path.read_text().strip() + available = content.replace('[', '').replace(']', '').split() + + # Find the currently selected mode (in brackets) + match = re.search(r'\[(\w+)\]', content) + if match: + mode_str = match.group(1) + if mode_str == 's2idle': + return SleepMode.S2IDLE, available, content + elif mode_str == 'deep': + return SleepMode.DEEP, available, content + + return SleepMode.UNKNOWN, available, content + except Exception: + return SleepMode.UNKNOWN, [], "" + + +def get_acpi_sleep_states() -> tuple[list[str], str]: + """ + Read available ACPI sleep states from /sys/power/state. + + Returns: + Tuple of (states_list, raw_content) + """ + state_path = Path('/sys/power/state') + + if not state_path.exists(): + return [], "" + + try: + content = state_path.read_text().strip() + return content.split(), content + except Exception: + return [], "" + + +def get_kernel_suspend_stats() -> tuple[int, int]: + """ + Read kernel suspend statistics from /sys/power/suspend_stats/. + + Returns: + Tuple of (success_count, fail_count) + """ + success = 0 + fail = 0 + + success_path = Path('/sys/power/suspend_stats/success') + fail_path = Path('/sys/power/suspend_stats/fail') + + try: + if success_path.exists(): + success = int(success_path.read_text().strip()) + except (ValueError, PermissionError): + pass + + try: + if fail_path.exists(): + fail = int(fail_path.read_text().strip()) + except (ValueError, PermissionError): + pass + + return success, fail + + +def get_psr_status() -> tuple[bool, str]: + """ + Check Panel Self Refresh (PSR) status for AMD and Intel GPUs. + + Returns: + Tuple of (psr_enabled, status_string) + """ + # Try AMD GPU PSR status + psr_paths = [ + '/sys/kernel/debug/dri/0/amdgpu_dm_dsc_disable', + '/sys/kernel/debug/dri/0/eDP-1/psr_state', + '/sys/kernel/debug/dri/1/eDP-1/psr_state', + ] + + # Try to find PSR info from debugfs - AMD + for card_num in range(4): + psr_path = Path(f'/sys/kernel/debug/dri/{card_num}/eDP-1/psr_capability') + if psr_path.exists(): + try: + content = psr_path.read_text().strip() + if content: + # Parse PSR capability info + enabled = 'enabled' in content.lower() or 'dc_version' in content.lower() + return enabled, content[:100] + except (PermissionError, OSError): + pass + + # Fixed: Try Intel PSR status paths + for card_num in range(4): + intel_psr_path = Path(f'/sys/kernel/debug/dri/{card_num}/i915_edp_psr_status') + if intel_psr_path.exists(): + try: + content = intel_psr_path.read_text().strip() + if content: + enabled = 'enabled' in content.lower() or 'active' in content.lower() + # Extract just the key status line + for line in content.split('\n'): + if 'PSR' in line or 'Enabled' in line or 'Status' in line: + return enabled, line.strip()[:100] + return enabled, content[:100] + except (PermissionError, OSError): + pass + + # Try alternative method via dmesg parsing would happen in log analysis + return False, "" + + +def get_last_suspend_resume_times(log_content: str) -> tuple[str, str]: + """ + Extract the most recent suspend and resume timestamps from logs. + + Args: + log_content: Log content to parse + + Returns: + Tuple of (last_suspend_time, last_resume_time) + """ + last_suspend = "" + last_resume = "" + + # Patterns to match suspend/resume with timestamps + suspend_patterns = [ + r'(\w+\s+\d+\s+\d+:\d+:\d+).*PM: suspend entry', + r'(\w+\s+\d+\s+\d+:\d+:\d+).*PM: Entering mem sleep', + r'\[(\w+\s+\w+\s+\d+\s+\d+:\d+:\d+\s+\d+)\].*PM: suspend entry', + ] + + resume_patterns = [ + r'(\w+\s+\d+\s+\d+:\d+:\d+).*PM: suspend exit', + r'(\w+\s+\d+\s+\d+:\d+:\d+).*PM: resume', + r'\[(\w+\s+\w+\s+\d+\s+\d+:\d+:\d+\s+\d+)\].*PM: suspend exit', + ] + + for line in log_content.split('\n'): + for pattern in suspend_patterns: + match = re.search(pattern, line, re.IGNORECASE) + if match: + last_suspend = match.group(1) + break + + for pattern in resume_patterns: + match = re.search(pattern, line, re.IGNORECASE) + if match: + last_resume = match.group(1) + break + + return last_suspend, last_resume + + +def count_suspend_resume_events(log_content: str) -> tuple[int, int, int, int]: + """ + Count suspend/resume events from log content. + + Args: + log_content: The log file content to analyze + + Returns: + Tuple of (suspend_count, resume_count, failed_suspend, failed_resume) + """ + suspend_count = 0 + resume_count = 0 + failed_suspend = 0 + failed_resume = 0 + + # Fixed: Only use patterns that indicate actual suspend ENTRY, not intermediate steps + # Previous bug: "Syncing filesystems" and "Freezing user space" happen during + # every suspend, causing 3-4x overcounting + suspend_patterns = [ + r'PM: suspend entry', + r'PM: Entering mem sleep', + ] + + # Patterns for successful resume + resume_patterns = [ + r'PM: suspend exit', + r'PM: Finishing wakeup', + ] + + # Patterns for failed suspend + failed_suspend_patterns = [ + r'PM:.*suspend.*failed', + r'suspend.*abort', + r'Failed to suspend', + r'Suspend failed', + ] + + # Patterns for failed resume - ONLY system-level PM failures + # NOT component errors like USB/GPU/WiFi resume hiccups which are + # normal and don't indicate a failed system resume + failed_resume_patterns = [ + r'^.*PM:.*resume from.*failed', + r'^.*PM: Some devices failed to resume', + r'^.*PM: noirq resume of devices failed', + r'^.*PM: late resume of devices failed', + ] + + for line in log_content.split('\n'): + line_lower = line.lower() + + for pattern in suspend_patterns: + if re.search(pattern, line, re.IGNORECASE): + suspend_count += 1 + break + + for pattern in resume_patterns: + if re.search(pattern, line, re.IGNORECASE): + resume_count += 1 + break + + for pattern in failed_suspend_patterns: + if re.search(pattern, line, re.IGNORECASE): + failed_suspend += 1 + break + + for pattern in failed_resume_patterns: + if re.search(pattern, line, re.IGNORECASE): + failed_resume += 1 + break + + return suspend_count, resume_count, failed_suspend, failed_resume + + +def find_resume_errors(log_content: str) -> list[str]: + """ + Find specific resume error messages in log content. + + Returns: + List of error messages related to resume failures + """ + errors = [] + + error_patterns = [ + (r'PM:.*Device.*failed to resume', 'Device resume failure'), + (r'ACPI.*resume.*failed', 'ACPI resume failure'), + (r'USB.*resume.*failed', 'USB device resume failure'), + (r'nvme.*resume.*failed', 'NVMe resume failure'), + (r'amdgpu.*resume.*failed', 'GPU resume failure'), + (r'i915.*resume.*failed', 'Intel GPU resume failure'), + (r'iwlwifi.*resume.*failed', 'WiFi resume failure'), + ] + + seen = set() + + for line in log_content.split('\n'): + for pattern, desc in error_patterns: + if re.search(pattern, line, re.IGNORECASE): + # Extract key info, deduplicate + key = f"{desc}: {line[:100]}" + if key not in seen: + seen.add(key) + errors.append(line.strip()) + + return errors + + +def find_amd_pmc_issues(log_content: str) -> list[str]: + """ + Find AMD PMC (Power Management Controller) issues in log content. + + AMD PMC issues can cause: + - Failed suspend + - Slow resume + - High power consumption in s2idle + + Returns: + List of AMD PMC related issues + """ + issues = [] + seen = set() + + pmc_patterns = [ + r'amd_pmc.*timeout', + r'amd_pmc.*failed', + r'amd_pmc.*SMU.*error', + r'amd_pmc.*response.*timeout', + r'amd_pmc.*command.*failed', + r'amd_pmc.*not.*responding', + ] + + for line in log_content.split('\n'): + for pattern in pmc_patterns: + if re.search(pattern, line, re.IGNORECASE): + # Deduplicate similar messages + key = line[:80] + if key not in seen: + seen.add(key) + issues.append(line.strip()) + + return issues + + +def find_sleep_blockers_in_logs(log_content: str) -> list[S2IdleBlocker]: + """ + Detect sleep blockers from log patterns. + + Detects 20+ specific patterns including: + - AMD/Intel GPU suspend failures + - USB device blocking/waking + - NVMe not entering low power + - Goodix fingerprint reader (Framework) + - Thunderbolt wake + - Intel WiFi wake + - Audio codec issues + - S0ix substate failures + - EC (Embedded Controller) blocking + - ACPI wakeup events + + Returns: + List of S2IdleBlocker with device, reason, and fix + """ + blockers = [] + seen_devices = set() + + # Pattern: (regex, device, reason, fix) + blocker_patterns = [ + # GPU issues + (r'amdgpu.*suspend.*failed', 'AMD GPU', 'GPU failed to suspend', + 'Try: amdgpu.runpm=0 kernel parameter or update GPU firmware'), + (r'amdgpu.*timeout.*waiting', 'AMD GPU', 'GPU timeout during power state change', + 'Try: echo high > /sys/class/drm/card0/device/power_dpm_force_performance_level'), + (r'i915.*suspend.*failed', 'Intel GPU', 'GPU failed to suspend', + 'Try: i915.enable_dc=0 kernel parameter'), + (r'i915.*timeout.*waiting', 'Intel GPU', 'GPU timeout during suspend', + 'Update Intel graphics driver or try i915.enable_psr=0'), + + # USB issues + (r'usb.*suspend.*failed', 'USB device', 'USB device blocking suspend', + 'Identify device with lsusb, check /sys/bus/usb/devices/*/power/control'), + (r'usb.*wakeup.*enabled', 'USB device', 'USB device configured as wakeup source', + 'Disable with: echo disabled > /sys/bus/usb/devices/X/power/wakeup'), + (r'usb.*reset.*resume', 'USB device', 'USB device needs reset on resume', + 'May indicate USB device firmware issue or power management incompatibility'), + + # NVMe issues + (r'nvme.*APST.*disabled', 'NVMe SSD', 'NVMe power saving disabled', + 'Enable with: nvme_core.default_ps_max_latency_us=5500'), + (r'nvme.*not.*entering.*low.*power', 'NVMe SSD', 'NVMe not entering low power state', + 'Check drive firmware, try: nvme_core.default_ps_max_latency_us=0'), + (r'nvme.*suspend.*failed', 'NVMe SSD', 'NVMe suspend failed', + 'Update NVMe firmware or try nvme_core.default_ps_max_latency_us=0'), + + # Framework-specific: Goodix fingerprint reader + (r'goodix.*suspend.*failed', 'Goodix fingerprint', 'Fingerprint reader blocking suspend', + 'Disable fingerprint in BIOS or blacklist goodix module'), + (r'goodix.*timeout', 'Goodix fingerprint', 'Fingerprint reader timeout', + 'echo "blacklist goodix" >> /etc/modprobe.d/blacklist.conf'), + + # Thunderbolt + (r'thunderbolt.*wake', 'Thunderbolt', 'Thunderbolt causing wake', + 'Disable Thunderbolt wake in BIOS or: echo disabled > /sys/bus/pci/devices/*/power/wakeup'), + (r'thunderbolt.*suspend.*failed', 'Thunderbolt', 'Thunderbolt suspend failed', + 'Disconnect Thunderbolt devices before suspend'), + + # Intel WiFi + (r'iwlwifi.*wakeup', 'Intel WiFi', 'WiFi configured as wake source', + 'Disable with: iw phy phy0 wowlan disable'), + (r'iwlwifi.*suspend.*failed', 'Intel WiFi', 'WiFi failed to suspend', + 'Try: iwlwifi.power_save=0 or update firmware'), + + # Audio codec + (r'snd_hda.*suspend.*failed', 'Audio codec', 'Audio codec blocking suspend', + 'Try: snd_hda_intel.power_save=1 snd_hda_intel.power_save_controller=Y'), + (r'sof.*suspend.*failed', 'SOF Audio', 'Sound Open Firmware suspend failed', + 'Update SOF firmware or try legacy HDA driver'), + + # EC (Embedded Controller) + (r'ec.*block.*sleep', 'EC', 'Embedded Controller blocking sleep', + 'Likely firmware issue - check for BIOS update'), + (r'ACPI.*EC.*timeout', 'EC', 'EC communication timeout', + 'Try: ec_intr=0 kernel parameter'), + + # ACPI wakeup + (r'ACPI.*wakeup.*GLAN', 'LAN', 'LAN configured for wake-on-LAN', + 'Disable WoL: ethtool -s eth0 wol d'), + (r'ACPI.*wakeup.*XHC', 'XHCI', 'USB controller wake enabled', + 'echo XHC > /proc/acpi/wakeup to toggle'), + (r'ACPI.*wakeup.*RP0[0-9]', 'PCIe Root Port', 'PCIe device wake enabled', + 'Check /proc/acpi/wakeup and toggle relevant device'), + + # s0ix failures + (r's0ix.*fail', 's0ix', 's0ix entry failed', + 'Check /sys/kernel/debug/pmc_core/substate_requirements (Intel) or amd_pmc (AMD)'), + (r'SLPS0.*fail', 's0ix', 'SLPS0 (s0ix) check failed', + 'BIOS/firmware may not support s0ix properly'), + + # Power management general + (r'PM:.*Device.*failed.*suspend', 'Unknown device', 'Device failed to suspend', + 'Check dmesg for specific device name'), + ] + + for line in log_content.split('\n'): + for pattern, device, reason, fix in blocker_patterns: + if re.search(pattern, line, re.IGNORECASE): + # Deduplicate by device + if device not in seen_devices: + seen_devices.add(device) + blockers.append(S2IdleBlocker( + device=device, + reason=reason, + fix=fix, + source=line.strip()[:100] + )) + + return blockers + + +def check_s2idle_status() -> tuple[bool, Optional[str]]: + """ + Check if s2idle is working correctly. + + Returns: + Tuple of (is_working, error_message) + """ + # Check if s2idle is the current mode + mode, available, _ = get_current_sleep_mode() + + if mode != SleepMode.S2IDLE: + if 's2idle' in available: + return False, f"s2idle available but not active. Current mode: {mode.value}" + else: + return False, "s2idle not available on this system" + + # Check for AMD-specific s2idle requirements + try: + result = subprocess.run( + ['cat', '/sys/power/pm_debug_messages'], + capture_output=True, + text=True, + timeout=2 + ) + # If we can read this, debug messages are available + except Exception: + pass + + return True, None + + +def check_sleep_status(log_content: Optional[str] = None) -> SleepStatus: + """ + Comprehensive sleep status check. + + NEW FUNCTION - Reports: + - Sleep mode (s2idle vs deep) + - s2idle status + - ACPI states + - Suspend/resume counts + - Inhibitors + - Resume errors + - AMD PMC issues + - Kernel suspend stats + - PSR status + - Last suspend/resume times + + Args: + log_content: Optional log content to analyze for suspend/resume events + + Returns: + SleepStatus object with all findings + """ + status = SleepStatus() + + # Get current sleep configuration (with raw evidence) + status.current_mode, status.available_modes, status.mem_sleep_raw = get_current_sleep_mode() + status.acpi_states, status.state_raw = get_acpi_sleep_states() + + # Get kernel suspend stats + status.kernel_suspend_success, status.kernel_suspend_fail = get_kernel_suspend_stats() + + # Get PSR status + status.psr_enabled, status.psr_status = get_psr_status() + + # Check s2idle status + s2idle_ok, s2idle_error = check_s2idle_status() + status.s2idle_enabled = s2idle_ok + if not s2idle_ok and s2idle_error: + status.issues.append(s2idle_error) + status.is_healthy = False + + # Inhibitors removed - they provide no useful info + # Only log-based blocker detection matters + + # If no log content provided, read kernel logs for this boot + # IMPORTANT: Use journalctl -k -b first, NOT dmesg -T. + # dmesg -T is unreliable after suspend/resume: the monotonic clock + # pauses during suspend, so dmesg -T recalculates wall-clock times + # incorrectly (often producing future dates). + if not log_content: + try: + result = subprocess.run( + ['journalctl', '-k', '--no-pager', '-b'], + capture_output=True, + text=True, + timeout=15 + ) + if result.returncode == 0 and result.stdout.strip(): + log_content = result.stdout + except (subprocess.TimeoutExpired, FileNotFoundError): + pass + + # Fallback to dmesg only if journalctl unavailable + if not log_content: + try: + result = subprocess.run( + ['sudo', 'dmesg', '-T'], + capture_output=True, + text=True, + timeout=10 + ) + if result.returncode == 0 and result.stdout.strip(): + log_content = result.stdout + except (subprocess.TimeoutExpired, FileNotFoundError): + pass + + # Analyze log content if available - do this BEFORE PMC blocker check + # so we know how many suspend attempts there were + if log_content: + # Count events + (status.suspend_count, status.resume_count, + status.failed_suspend_count, status.failed_resume_count) = \ + count_suspend_resume_events(log_content) + + # Get last suspend/resume timestamps + status.last_suspend_time, status.last_resume_time = get_last_suspend_resume_times(log_content) + + # Find specific errors + status.resume_errors = find_resume_errors(log_content) + status.amd_pmc_issues = find_amd_pmc_issues(log_content) + + # Sleep blockers - ONLY from log analysis, no sysfs queries that cause false positives + + # Continue analyzing log content if available + if log_content: + + # Find sleep blockers in logs + log_blockers = find_sleep_blockers_in_logs(log_content) + status.blockers.extend(log_blockers) + + # Check PSR status from logs if not found via sysfs + if not status.psr_status: + psr_match = re.search(r'PSR.*(?:DC|sink).*version.*(\d+)', log_content, re.IGNORECASE) + if psr_match: + status.psr_enabled = True + # Look for full PSR status line + for line in log_content.split('\n'): + if 'PSR' in line and ('DC' in line or 'sink' in line): + status.psr_status = line.strip()[-80:] + break + + # Detect PSR-related issues (screen blinking/flickering on resume) + psr_issue_patterns = [ + (r'PSR.*exit.*error', 'PSR exit error detected'), + (r'PSR.*timeout', 'PSR timeout'), + (r'amdgpu.*PSR.*fail', 'AMD GPU PSR failure'), + (r'i915.*PSR.*error', 'Intel PSR error'), + (r'drm.*underrun.*resume', 'Display underrun on resume'), + (r'flickering.*resume|resume.*flickering', 'Screen flickering on resume'), + ] + for pattern, description in psr_issue_patterns: + if re.search(pattern, log_content, re.IGNORECASE): + if description not in status.psr_issues: + status.psr_issues.append(description) + + # Check health based on findings + if status.failed_suspend_count > 0: + status.is_healthy = False + status.issues.append(f"{status.failed_suspend_count} failed suspend(s)") + + if status.failed_resume_count > 0: + status.is_healthy = False + status.issues.append(f"{status.failed_resume_count} failed resume(s)") + + if status.resume_errors: + status.is_healthy = False + status.issues.append(f"{len(status.resume_errors)} resume error(s)") + + if status.amd_pmc_issues: + status.is_healthy = False + status.issues.append(f"{len(status.amd_pmc_issues)} AMD PMC issue(s)") + + # Mark unhealthy if we have blockers + if status.blockers: + status.is_healthy = False + status.issues.append(f"{len(status.blockers)} sleep blocker(s) detected") + + # Framework workaround services + status.disable_wakeup_service = Path('/etc/systemd/system/disable-wakeup.service').exists() + + return status + + +def format_sleep_status_report(status: SleepStatus) -> list[str]: + """ + Format sleep status for the diagnostic report. + + Returns: + List of report lines + """ + lines = [] + + lines.append("Sleep/Suspend Status:") + + # Current mode + mode_str = status.current_mode.value + if status.current_mode == SleepMode.S2IDLE: + lines.append(f" Mode: {mode_str} (modern standby) ✅") + elif status.current_mode == SleepMode.DEEP: + lines.append(f" Mode: {mode_str} (S3 sleep)") + else: + lines.append(f" Mode: {mode_str}") + + # Evidence - raw file contents + if status.mem_sleep_raw: + lines.append(f" Evidence: /sys/power/mem_sleep = {status.mem_sleep_raw}") + if status.state_raw: + lines.append(f" Evidence: /sys/power/state = {status.state_raw}") + + # Available modes + if status.available_modes: + lines.append(f" Available modes: {', '.join(status.available_modes)}") + + # ACPI states + if status.acpi_states: + lines.append(f" ACPI states: {', '.join(status.acpi_states)}") + + # Kernel suspend stats (from /sys/power/suspend_stats/) + if status.kernel_suspend_success > 0 or status.kernel_suspend_fail > 0: + lines.append(f" Kernel suspend stats (since boot): {status.kernel_suspend_success} successful, {status.kernel_suspend_fail} failed") + + # Log suspend/resume counts with timestamps + if status.suspend_count > 0 or status.resume_count > 0: + lines.append(f" Log suspend cycles (since boot): {status.suspend_count} suspend, {status.resume_count} resume") + if status.last_suspend_time: + lines.append(f" Last suspend: {status.last_suspend_time}") + if status.last_resume_time: + lines.append(f" Last resume: {status.last_resume_time}") + if status.failed_suspend_count > 0: + lines.append(f" ❌ Failed suspends: {status.failed_suspend_count}") + if status.failed_resume_count > 0: + lines.append(f" ❌ Failed resumes: {status.failed_resume_count}") + + # PSR (Panel Self Refresh) status + if status.psr_status or status.psr_issues: + lines.append(f" PSR (Panel Self Refresh): {status.psr_status if status.psr_status else 'Enabled'}") + # Only show fix advice if actual PSR issues were detected + if status.psr_issues: + for issue in status.psr_issues: + lines.append(f" ⚠️ {issue}") + lines.append(" Fix: Try kernel parameter amdgpu.dcdebugmask=0x10 (AMD) or i915.enable_psr=0 (Intel)") + + # AMD PMC issues (from logs) + if status.amd_pmc_issues: + lines.append(f" ⚠️ AMD PMC issues: {len(status.amd_pmc_issues)}") + + # Sleep blockers (NEW) + if status.blockers: + lines.append(f" ⚠️ Sleep blockers detected: {len(status.blockers)}") + for blocker in status.blockers: + lines.append(f" Device: {blocker.device}") + lines.append(f" Reason: {blocker.reason}") + lines.append(f" Fix: {blocker.fix}") + + # Framework workaround services + if status.disable_wakeup_service: + lines.append(" disable-wakeup.service: ✅ installed") + + return lines diff --git a/fw-log-tool/framework_diagnostic/system_info.py b/fw-log-tool/framework_diagnostic/system_info.py new file mode 100644 index 0000000..934fdac --- /dev/null +++ b/fw-log-tool/framework_diagnostic/system_info.py @@ -0,0 +1,433 @@ +""" +System information detection. + +Detects: +- Kernel version +- Desktop environment (GNOME, KDE, XFCE, etc.) +- Session type (Wayland, X11) +- Distribution info +- Power management daemon (ppd, tuned-ppd, tuned, TLP) +""" + +import os +from dataclasses import dataclass, field +from pathlib import Path +from typing import Optional + +from .utils import run_command + + +@dataclass +class SystemInfo: + """System information.""" + kernel_version: str = "" + + # Desktop environment + desktop_environment: str = "" # GNOME, KDE, XFCE, etc. + session_type: str = "" # wayland, x11 + + # Distribution + distro_name: str = "" + distro_id: str = "" + distro_version: str = "" + + # Power management + power_daemon: str = "" # power-profiles-daemon, tuned-ppd, tuned, tlp + power_profile: str = "" # balanced, power-saver, performance, or tuned profile name + power_conflicts: list[str] = field(default_factory=list) # e.g. ["ppd + tlp both active"] + + # Framework recommended configs + io_uring_disabled: Optional[bool] = None # True = sysctl conf exists + + +def get_kernel_version() -> str: + """Get the running kernel version.""" + rc, stdout, _ = run_command(['uname', '-r']) + if rc == 0: + return stdout.strip() + return "Unknown" + + +def get_desktop_environment() -> tuple[str, str]: + """ + Detect desktop environment and session type. + + Uses multiple methods: + 1. Environment variables (XDG_CURRENT_DESKTOP, XDG_SESSION_TYPE) + 2. loginctl session info + 3. Process detection fallback + + Returns: + Tuple of (desktop_environment, session_type) + """ + desktop = "" + session = "" + + # Method 1: Check environment variables + desktop = os.environ.get('XDG_CURRENT_DESKTOP', '') + session = os.environ.get('XDG_SESSION_TYPE', '') + + # "tty" means we're in a root shell (su - or sudo), not the actual session + if session == 'tty': + session = '' + + # Method 2: If running as root/sudo, try loginctl + if not desktop: + sudo_user = os.environ.get('SUDO_USER', '') + if sudo_user: + # Get all sessions — user may have graphical + tty from sudo. + # We want the graphical one. + rc, stdout, _ = run_command(['loginctl', 'list-sessions', '--no-legend']) + if rc == 0: + best_desktop = '' + best_session = '' + for line in stdout.strip().split('\n'): + if sudo_user in line: + parts = line.split() + if not parts: + continue + session_id = parts[0] + + rc3, stdout3, _ = run_command([ + 'loginctl', 'show-session', session_id, + '-p', 'Type', '--value' + ]) + sess_type = stdout3.strip() if rc3 == 0 else '' + + rc2, stdout2, _ = run_command([ + 'loginctl', 'show-session', session_id, + '-p', 'Desktop', '--value' + ]) + sess_desktop = stdout2.strip() if rc2 == 0 else '' + + # Prefer wayland/x11 over tty + if sess_type in ('wayland', 'x11'): + best_session = sess_type + if sess_desktop: + best_desktop = sess_desktop + break # Found graphical session, done + elif not best_session: + best_session = sess_type + best_desktop = sess_desktop + + if best_desktop: + desktop = best_desktop + if best_session: + session = best_session + + # Method 3: Process detection fallback + if not desktop: + # Check for common desktop environment processes + desktop_processes = { + 'gnome-shell': 'GNOME', + 'gnome-session': 'GNOME', + 'plasmashell': 'KDE', + 'kwin': 'KDE', + 'xfce4-session': 'XFCE', + 'xfce4-panel': 'XFCE', + 'cinnamon': 'Cinnamon', + 'mate-session': 'MATE', + 'mate-panel': 'MATE', + 'budgie-panel': 'Budgie', + 'lxqt-session': 'LXQt', + 'lxsession': 'LXDE', + 'sway': 'Sway', + 'hyprland': 'Hyprland', + 'i3': 'i3', + 'openbox': 'Openbox', + } + + rc, stdout, _ = run_command(['ps', '-e', '-o', 'comm=']) + if rc == 0: + running = set(stdout.strip().split('\n')) + for proc, de_name in desktop_processes.items(): + if proc in running: + desktop = de_name + break + + # Detect session type if not found + if not session: + # Check for Wayland + if os.environ.get('WAYLAND_DISPLAY'): + session = 'wayland' + elif os.environ.get('DISPLAY'): + # DISPLAY survives sudo, but Xwayland also sets DISPLAY on + # Wayland sessions. Check for Xwayland before concluding x11. + rc, stdout, _ = run_command(['pgrep', '-x', 'Xwayland']) + if rc == 0 and stdout.strip(): + session = 'wayland' + else: + session = 'x11' + else: + # Env vars stripped (su -). Try loginctl for any graphical session. + rc, stdout, _ = run_command(['loginctl', 'list-sessions', '--no-legend']) + if rc == 0: + for line in stdout.strip().split('\n'): + parts = line.split() + if not parts: + continue + rc2, stdout2, _ = run_command([ + 'loginctl', 'show-session', parts[0], + '-p', 'Type', '--value' + ]) + sess_type = stdout2.strip() if rc2 == 0 else '' + if sess_type in ('wayland', 'x11'): + session = sess_type + break + + # Last resort: process detection + if not session: + rc, stdout, _ = run_command(['pgrep', '-x', 'Xorg']) + if rc == 0 and stdout.strip(): + session = 'x11' + else: + rc, stdout, _ = run_command(['pgrep', '-x', 'Xwayland']) + if rc == 0 and stdout.strip(): + session = 'wayland' + + return desktop, session + + +def get_distro_info() -> tuple[str, str, str]: + """ + Get distribution information from /etc/os-release. + + Returns: + Tuple of (pretty_name, id, version_id) + """ + pretty_name = "" + distro_id = "" + version_id = "" + + os_release = Path('/etc/os-release') + if os_release.exists(): + try: + content = os_release.read_text() + for line in content.split('\n'): + if line.startswith('PRETTY_NAME='): + pretty_name = line.split('=', 1)[1].strip('"') + elif line.startswith('ID='): + distro_id = line.split('=', 1)[1].strip('"') + elif line.startswith('VERSION_ID='): + version_id = line.split('=', 1)[1].strip('"') + except Exception: + pass + + return pretty_name, distro_id, version_id + + +def check_service_active(service: str) -> bool: + """Check if a systemd service is active.""" + rc, stdout, _ = run_command(['systemctl', 'is-active', service]) + return rc == 0 and stdout.strip() == 'active' + + +def check_service_enabled(service: str) -> bool: + """Check if a systemd service is enabled.""" + rc, stdout, _ = run_command(['systemctl', 'is-enabled', service]) + return rc == 0 and stdout.strip() in ('enabled', 'enabled-runtime') + + +def get_power_profile() -> tuple[str, str]: + """ + Detect power profile daemon and current profile. + + Detection order: + 1. Check which services are actually running + 2. Query the appropriate tool for current profile + + Supports: + - power-profiles-daemon (ppd) + - tuned-ppd (tuned with ppd compatibility) + - tuned (standalone) + - TLP + + Returns: + Tuple of (daemon_name, current_profile) + """ + # Check service states first + ppd_active = check_service_active('power-profiles-daemon') + tuned_ppd_active = check_service_active('tuned-ppd') + tuned_active = check_service_active('tuned') + tlp_active = check_service_active('tlp') + + # Priority 1: tuned-ppd (provides ppd interface but uses tuned backend) + if tuned_ppd_active: + rc, stdout, _ = run_command(['powerprofilesctl', 'get']) + if rc == 0: + profile = stdout.strip() + # Also get the underlying tuned profile for extra info + rc2, stdout2, _ = run_command(['tuned-adm', 'active']) + if rc2 == 0: + # Parse "Current active profile: " + for line in stdout2.strip().split('\n'): + if 'Current active profile:' in line: + tuned_profile = line.split(':', 1)[1].strip() + return 'tuned-ppd', f"{profile} (tuned: {tuned_profile})" + return 'tuned-ppd', profile + + # Priority 2: power-profiles-daemon (standalone ppd) + if ppd_active: + rc, stdout, _ = run_command(['powerprofilesctl', 'get']) + if rc == 0: + return 'power-profiles-daemon', stdout.strip() + + # Priority 3: standalone tuned (no ppd interface) + if tuned_active and not tuned_ppd_active: + rc, stdout, _ = run_command(['tuned-adm', 'active']) + if rc == 0: + for line in stdout.strip().split('\n'): + if 'Current active profile:' in line: + profile = line.split(':', 1)[1].strip() + return 'tuned', profile + # Fallback: just report tuned is active + return 'tuned', 'active (profile unknown)' + + # Priority 4: TLP + if tlp_active: + rc, stdout, _ = run_command(['tlp-stat', '-s']) + if rc == 0: + mode = 'active' + # Parse TLP status output for mode + for line in stdout.split('\n'): + if 'Mode' in line and '=' in line: + mode = line.split('=', 1)[1].strip() + break + return 'tlp', mode + return 'tlp', 'active' + + # Fallback: Try commands even if service check failed + # (some systems might not have systemd or service names differ) + + # Try powerprofilesctl + rc, stdout, _ = run_command(['powerprofilesctl', 'get']) + if rc == 0: + profile = stdout.strip() + # Determine which backend + if check_service_enabled('tuned-ppd'): + return 'tuned-ppd', profile + return 'power-profiles-daemon', profile + + # Try tuned-adm + rc, stdout, _ = run_command(['tuned-adm', 'active']) + if rc == 0: + for line in stdout.strip().split('\n'): + if 'Current active profile:' in line: + profile = line.split(':', 1)[1].strip() + return 'tuned', profile + + # Try tlp-stat + rc, stdout, _ = run_command(['tlp-stat', '-s']) + if rc == 0: + return 'tlp', 'active' + + return '', '' + + +def detect_power_conflicts() -> list[str]: + """Detect conflicting power management daemons. + + Multiple active power managers fight over CPU frequency, turbo boost, + and platform profile, causing erratic performance, battery drain, + and thermal issues. This checks for actual conflicts, not just + multiple installed packages. + + Returns: + List of human-readable conflict descriptions + """ + conflicts = [] + + # Check which services are active (running right now) + ppd_active = check_service_active('power-profiles-daemon') + tuned_ppd_active = check_service_active('tuned-ppd') + tuned_active = check_service_active('tuned') + tlp_active = check_service_active('tlp') + + # Check which are enabled (start on boot) + ppd_enabled = check_service_enabled('power-profiles-daemon') + tuned_ppd_enabled = check_service_enabled('tuned-ppd') + tuned_enabled = check_service_enabled('tuned') + tlp_enabled = check_service_enabled('tlp') + + # tuned-ppd is designed to coexist with tuned — that's not a conflict. + # But ppd + tlp or ppd + tuned (standalone) are conflicts. + + # Active conflicts (running simultaneously right now) + if ppd_active and tlp_active: + conflicts.append("⚠️ power-profiles-daemon AND tlp both active — they will fight over power policy") + + if ppd_active and tuned_active and not tuned_ppd_active: + conflicts.append("⚠️ power-profiles-daemon AND tuned both active — they will fight over power policy") + + if tlp_active and tuned_active: + conflicts.append("⚠️ tlp AND tuned both active — they will fight over power policy") + + # Enabled-but-not-running (will conflict on next boot) + if ppd_enabled and tlp_enabled and not (ppd_active and tlp_active): + if not ppd_active or not tlp_active: + # One might have lost the race this boot, but both will try next boot + conflicts.append("ℹ️ power-profiles-daemon AND tlp both enabled — potential conflict on next boot") + + if ppd_enabled and tuned_enabled and not tuned_ppd_enabled: + if not (ppd_active and tuned_active): + conflicts.append("ℹ️ power-profiles-daemon AND tuned both enabled — potential conflict on next boot") + + if tlp_enabled and tuned_enabled: + if not (tlp_active and tuned_active): + conflicts.append("ℹ️ tlp AND tuned both enabled — potential conflict on next boot") + + return conflicts + + +def detect_system_info() -> SystemInfo: + """Detect all system information.""" + info = SystemInfo() + + info.kernel_version = get_kernel_version() + info.desktop_environment, info.session_type = get_desktop_environment() + info.distro_name, info.distro_id, info.distro_version = get_distro_info() + info.power_daemon, info.power_profile = get_power_profile() + info.power_conflicts = detect_power_conflicts() + info.io_uring_disabled = Path('/etc/sysctl.d/10-disable-io_uring.conf').exists() + + return info + + +def format_system_info_report(info: SystemInfo) -> list[str]: + """Format system info for the diagnostic report.""" + lines = [] + + lines.append("System Information:") + lines.append(f" Kernel: {info.kernel_version}") + + # Desktop environment with session type + if info.desktop_environment: + if info.session_type: + lines.append(f" Desktop: {info.desktop_environment} ({info.session_type})") + else: + lines.append(f" Desktop: {info.desktop_environment}") + elif info.session_type: + lines.append(f" Session: {info.session_type}") + else: + lines.append(" Desktop: Unknown") + + # Distribution + if info.distro_name: + lines.append(f" Distribution: {info.distro_name}") + + # Power profile + if info.power_daemon: + lines.append(f" Power Management: {info.power_daemon}") + lines.append(f" Power Profile: {info.power_profile}") + else: + lines.append(" Power Management: None detected (no ppd/tuned/tlp)") + + # Power management conflicts + for conflict in info.power_conflicts: + lines.append(f" {conflict}") + + # Framework recommended configs + if info.io_uring_disabled: + lines.append(" io_uring disabled: ✅ sysctl config installed") + + return lines diff --git a/fw-log-tool/framework_diagnostic/thermal.py b/fw-log-tool/framework_diagnostic/thermal.py new file mode 100644 index 0000000..99a8220 --- /dev/null +++ b/fw-log-tool/framework_diagnostic/thermal.py @@ -0,0 +1,274 @@ +""" +Thermal monitoring and temperature analysis. +""" + +import re +from dataclasses import dataclass, field +from typing import Optional +from enum import Enum + +from .hardware import CPUVendor, AMDGeneration +from .utils import run_command + + +class ThermalStatus(Enum): + """Thermal status levels.""" + NORMAL = 'normal' + ELEVATED = 'elevated' + WARNING = 'warning' + CRITICAL = 'critical' + EMERGENCY = 'emergency' + + +@dataclass +class ThermalReading: + """A single thermal reading.""" + sensor: str + temp_celsius: float + source: str # Tctl, Package, Core, etc. + + +@dataclass +class ThermalInfo: + """Complete thermal information.""" + cpu_temp: Optional[float] = None + cpu_source: str = "" + gpu_temp: Optional[float] = None + nvme_temp: Optional[float] = None + + status: ThermalStatus = ThermalStatus.NORMAL + readings: list[ThermalReading] = field(default_factory=list) + + # Thresholds (set based on CPU type) + watch_threshold: int = 80 + warning_threshold: int = 85 + critical_threshold: int = 90 + emergency_threshold: int = 100 + + +def parse_sensors_output() -> dict[str, float]: + """Parse output from the 'sensors' command.""" + temps = {} + + rc, stdout, _ = run_command(['sensors']) + if rc != 0: + return temps + + for line in stdout.split('\n'): + # Parse lines like "Tctl: +45.0°C" + # or "Package id 0: +42.0°C" + # or "Core 0: +40.0°C" + # or "edge: +38.0°C" + # or "Composite: +35.0°C" + + match = re.match(r'^\s*([^:]+):\s*\+?([\d.]+)°?C', line) + if match: + sensor_name = match.group(1).strip() + temp = float(match.group(2)) + temps[sensor_name] = temp + + return temps + + +def get_cpu_temperature(temps: dict[str, float]) -> tuple[Optional[float], str]: + """ + Get CPU temperature from sensors data. + + Priority order: + 1. Tctl (AMD) + 2. Package (Intel) + 3. Core 0 + 4. cpu@4c (some ARM/other) + + Returns: + Tuple of (temperature, source_name) + """ + # Try Tctl first (AMD) + if 'Tctl' in temps: + return temps['Tctl'], 'Tctl' + + # Try Package (Intel) + for key in temps: + if 'Package' in key: + return temps[key], 'Package' + + # Try Core 0 + for key in temps: + if 'Core' in key and '0' in key: + return temps[key], 'Core' + + # Try cpu@4c + for key in temps: + if 'cpu' in key.lower(): + return temps[key], key + + return None, "" + + +def get_gpu_temperature(temps: dict[str, float]) -> Optional[float]: + """Get GPU temperature from sensors data.""" + # AMD GPU edge temp + if 'edge' in temps: + return temps['edge'] + + # Junction temp (can be higher) + if 'junction' in temps: + return temps['junction'] + + return None + + +def get_nvme_temperature(temps: dict[str, float]) -> Optional[float]: + """Get NVMe temperature from sensors data.""" + if 'Composite' in temps: + return temps['Composite'] + return None + + +def get_thermal_thresholds( + cpu_vendor: CPUVendor, + amd_generation: AMDGeneration, + is_framework: bool +) -> tuple[int, int, int, int]: + """ + Get thermal thresholds based on CPU type. + + Returns: + Tuple of (watch, warning, critical, emergency) temperatures + """ + if cpu_vendor == CPUVendor.AMD: + if amd_generation == AMDGeneration.MODERN: + # Modern AMD (Ryzen 7000/8000, AI 300) runs hot by design + # Tjmax is typically 100-105°C + return (90, 95, 100, 105) + else: + # Older AMD + return (85, 90, 95, 105) + elif cpu_vendor == CPUVendor.INTEL: + # Intel typically has Tjmax of 100°C + return (80, 85, 90, 100) + else: + # Unknown - use conservative thresholds + return (80, 85, 90, 100) + + +def evaluate_thermal_status( + temp: float, + watch: int, + warning: int, + critical: int, + emergency: int +) -> ThermalStatus: + """Evaluate thermal status based on temperature and thresholds.""" + if temp >= emergency: + return ThermalStatus.EMERGENCY + elif temp >= critical: + return ThermalStatus.CRITICAL + elif temp >= warning: + return ThermalStatus.WARNING + elif temp >= watch: + return ThermalStatus.ELEVATED + else: + return ThermalStatus.NORMAL + + +def check_current_temperatures( + cpu_vendor: CPUVendor = CPUVendor.UNKNOWN, + amd_generation: AMDGeneration = AMDGeneration.LEGACY, + is_framework: bool = False +) -> ThermalInfo: + """ + Check current system temperatures. + + Args: + cpu_vendor: CPU vendor for threshold calibration + amd_generation: AMD generation for threshold calibration + is_framework: Whether this is a Framework device + + Returns: + ThermalInfo with all readings and status + """ + info = ThermalInfo() + + # Get thresholds + watch, warning, critical, emergency = get_thermal_thresholds( + cpu_vendor, amd_generation, is_framework + ) + info.watch_threshold = watch + info.warning_threshold = warning + info.critical_threshold = critical + info.emergency_threshold = emergency + + # Parse sensors + temps = parse_sensors_output() + + # Get CPU temp + cpu_temp, source = get_cpu_temperature(temps) + if cpu_temp is not None: + info.cpu_temp = cpu_temp + info.cpu_source = source + info.readings.append(ThermalReading( + sensor='CPU', + temp_celsius=cpu_temp, + source=source + )) + + # Evaluate status + info.status = evaluate_thermal_status( + cpu_temp, watch, warning, critical, emergency + ) + + # Get GPU temp + gpu_temp = get_gpu_temperature(temps) + if gpu_temp is not None: + info.gpu_temp = gpu_temp + info.readings.append(ThermalReading( + sensor='GPU', + temp_celsius=gpu_temp, + source='edge' + )) + + # Get NVMe temp + nvme_temp = get_nvme_temperature(temps) + if nvme_temp is not None: + info.nvme_temp = nvme_temp + info.readings.append(ThermalReading( + sensor='NVMe', + temp_celsius=nvme_temp, + source='Composite' + )) + + return info + + +def format_thermal_report( + info: ThermalInfo, + cpu_vendor: CPUVendor, + amd_generation: AMDGeneration, + is_framework: bool +) -> list[str]: + """Format thermal info for the diagnostic report.""" + lines = [] + + lines.append("Thermal Status:") + + # Show raw sensor data + for reading in info.readings: + lines.append(f" {reading.sensor}: {reading.temp_celsius}°C ({reading.source})") + + # Show interpreted CPU status + if info.cpu_temp is not None: + threshold_info = "" + if is_framework: + if cpu_vendor == CPUVendor.AMD: + if amd_generation == AMDGeneration.MODERN: + threshold_info = f" (Modern AMD: runs hot by design - watch at {info.watch_threshold}°C, throttles at {info.warning_threshold}°C, critical at {info.critical_threshold}°C)" + else: + threshold_info = f" (Older AMD: watch at {info.watch_threshold}°C, warning at {info.warning_threshold}°C, critical at {info.critical_threshold}°C)" + elif cpu_vendor == CPUVendor.INTEL: + threshold_info = f" (Intel: watch at {info.watch_threshold}°C, warning at {info.warning_threshold}°C, critical at {info.critical_threshold}°C)" + + lines.append(f" Current CPU: {info.cpu_temp}°C via {info.cpu_source}{threshold_info}") + + return lines + diff --git a/fw-log-tool/framework_diagnostic/utils.py b/fw-log-tool/framework_diagnostic/utils.py new file mode 100644 index 0000000..5312196 --- /dev/null +++ b/fw-log-tool/framework_diagnostic/utils.py @@ -0,0 +1,32 @@ +""" +Shared utility functions. + +Centralizes command execution so it's not duplicated across +hardware.py, thermal.py, network.py, and system_info.py. +""" + +import subprocess + + +def run_command(cmd: list[str], timeout: int = 10, env=None) -> tuple[int, str, str]: + """ + Run a command and return (returncode, stdout, stderr). + + Returns (-1, "", ) on timeout or missing command. + + Args: + env: Optional environment dict. If provided, replaces the default + environment for the subprocess. + """ + try: + result = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout, env=env) + return result.returncode, result.stdout, result.stderr + except subprocess.TimeoutExpired: + return -1, "", "timeout" + except FileNotFoundError: + return -1, "", "command not found" + + +def run_sudo_command(cmd: list[str], timeout: int = 10) -> tuple[int, str, str]: + """Run a command with sudo.""" + return run_command(['sudo'] + cmd, timeout) diff --git a/fw-log-tool/fw_diag.pyz b/fw-log-tool/fw_diag.pyz new file mode 100644 index 0000000..77c2dd7 Binary files /dev/null and b/fw-log-tool/fw_diag.pyz differ diff --git a/fwupd.md b/fwupd.md new file mode 100644 index 0000000..f803c9c --- /dev/null +++ b/fwupd.md @@ -0,0 +1,45 @@ +# Firmware Update Manager (fwupd/LVFS) + +fwupd currently maintains two branches: 1.9.X and 2.0.X. +The former is the LTS branch that gets most bugs backported. +The latter the development branch where fixes and new features land first. + +## Versions in Distributions + +- Fedora + - [![Fedora 42 package](https://repology.org/badge/version-for-repo/fedora_42/fwupd.svg)](https://repology.org/project/fwupd/versions) + - [![Fedora Rawhide package](https://repology.org/badge/version-for-repo/fedora_rawhide/fwupd.svg)](https://repology.org/project/fwupd/versions) +- NixOS + - [![nixpkgs stable 25.05 package](https://repology.org/badge/version-for-repo/nix_stable_25_05/fwupd.svg)](https://repology.org/project/fwupd/versions) + - [![nixpkgs unstable package](https://repology.org/badge/version-for-repo/nix_unstable/fwupd.svg)](https://repology.org/project/fwupd/versions) +- Ubuntu + - [![Ubuntu 24.04 package](https://repology.org/badge/version-for-repo/ubuntu_24_04/fwupd.svg)](https://repology.org/project/fwupd/versions) + - [![Ubuntu 25.04 package](https://repology.org/badge/version-for-repo/ubuntu_25_04/fwupd.svg)](https://repology.org/project/fwupd/versions) + - [![Ubuntu 25.10 package](https://repology.org/badge/version-for-repo/ubuntu_25_10/fwupd.svg)](https://repology.org/project/fwupd/versions) +- [![Arch Linux package](https://repology.org/badge/version-for-repo/arch/fwupd.svg)](https://repology.org/project/fwupd/versions) + +## Framework 16 Keyboard Firmware Update + +Added in version 2.0.14 by pull request [#9094](https://github.com/fwupd/fwupd/pull/9094). + +Below are the links to view the latest available firmware versions: + +- [Laptop 16 Keyboard - ANSI](https://fwupd.org/lvfs/devices/work.frame.Laptop16.Inputmodules.ANSI) +- [Laptop 16 Keyboard - ISO](https://fwupd.org/lvfs/devices/work.frame.Laptop16.Inputmodules.ISO) +- [Laptop 16 RGB Macropad](https://fwupd.org/lvfs/devices/work.frame.Laptop16.Inputmodules.Macropad) +- [Laptop 16 Numpad](https://fwupd.org/lvfs/devices/work.frame.Laptop16.Inputmodules.Numpad) + +## Framework 12 Touchscreen Controller + +Added in version 2.0.14 by pull request [#9163](https://github.com/fwupd/fwupd/pull/9163). + +Currently there is no firmware update available - systems ship with the latest version from the factory. + +## Framework 12/13/16 Webcam Firmware + +Framework 13 and Framework 16 share the same camera module. The 2nd gen is updateable through fwupd. +Framework 12 camera module is also updateable through fwupd. + +Support for DFU update has been available in fwupd for a long time. + +Currently there is no firmware update available - systems ship with the latest version from the factory. diff --git a/goodix-moc-609c-v01000330.cab b/goodix-moc-609c-v01000330.cab deleted file mode 100644 index f507336..0000000 Binary files a/goodix-moc-609c-v01000330.cab and /dev/null differ diff --git a/hibernation/hibernate-fedora-automatic.md b/hibernation/hibernate-fedora-automatic.md new file mode 100644 index 0000000..616868e --- /dev/null +++ b/hibernation/hibernate-fedora-automatic.md @@ -0,0 +1,209 @@ +# Fedora 41/42 hibernation option (NOT Fedora official) +(Suspends to disk/powered off with saved state to partition) + +### Reminder: Secure boot must be off per the middle section below. + +> What is hibernate? [Learn more here.](https://knowledgebase.frame.work/en_us/hibernation-on-linux-BkL1N5ffJg) + +**NOTE:** Prefer to do this manually on Fedora? [Follow the manual guide here](https://github.com/FrameworkComputer/linux-docs/blob/main/hibernation/hibernate-fedora-manual-method.md#manual-guide-configuring-lid-close-and-hibernate-settings-on-linux). + +**NOTE:** If you feel strongly about using btrfs subvolumes, other approaches that are untested by us, below are some links for community guides on that front: + +- [GUIDE] Framework 16 Hibernate (w/ swapfile) Setup on Fedora 40: [Read the full guide here](https://community.frame.work/t/guide-framework-16-hibernate-w-swapfile-setup-on-fedora-40/53080/1) +- [Guide] Fedora 36+: Hibernation with enabled secure boot and full disk encryption (FDE) decrypting over TPM2: [Read the guide here](https://community.frame.work/t/guide-fedora-36-hibernation-with-enabled-secure-boot-and-full-disk-encryption-fde-decrypting-over-tpm2/25474) +- Subvolume Btrfs Hibernate Approach: [Read more about this approach here](https://terminal.space/tech/hibernating-is-easy-now/). + +Otherwise, continue below to use the partition with application method. + +**Tested successfully on:** + +- Framework Laptop 13 AMD Ryzen 7040 Series +- Framework Laptop 13 Intel® Core™ Ultra Series 1 +- Framework laptop 16 AMD Ryzen 7040 Series +- Secure boot [needs to be disabled](https://github.com/FrameworkComputer/linux-docs/blob/main/misc/secure-boot.md#secure-boot-explained) (<--This affects EFI updaters for example) + +**Guide Sections:** + +- [Partition Layout](#access-partition-layout) +- [Lid Close and Hibernate Settings Installation and Usage Guide](#lid-close-and-hibernate-settings-installation-and-usage-guide) + +Hibernation to disk in Linux using a swap partition is a valuable feature for preserving your system's state while completely powering off the machine. +It works by saving the contents of RAM to the swap partition and restoring it upon reboot. On modern laptops equipped with NVMe SSDs, hibernation and resume processes are significantly faster due to the high-speed read/write capabilities of NVMe drives. +This enhanced performance makes hibernation a practical and efficient option for energy conservation and session continuity without compromising user experience. + +**Note:** This is being released as a beta for user testing: + +- While tested working great internally, users may have improvements and tweaks to make it better. +- COPR is coming for the released version. This is designed with Framework Laptops in mind, but, should work with any compatible laptop. +- If you are comfortable adjusting the partition settings suggested below, go for it - do understand if something fails to and you deviate from the layout, you will be asked to redo the partitions as suggested by support to verify your settings. +- As you enter partition sizes as stated below, the actual size allotted will differ as that is how partitioning works. This is fine. + + + +## Setting Up Partition Layout for Fedora Installation + +### Partition Layout + +1. During Fedora installation, access **System, Install Destination**. +2. Choose **Custom** partitioning and click **Done**. +3. Ignore the **Encrypt my Data** checkbox since we will handle encryption separately. + + +**NOTE:** If this is a drive without any partitions on it, you will see something like this: +[![Fresh Installation](https://raw.githubusercontent.com/FrameworkComputer/linux-docs/refs/heads/main/hibernation/images/1.png)](https://raw.githubusercontent.com/FrameworkComputer/linux-docs/refs/heads/main/hibernation/images/1.png) + +If you have existing partitions, you will need to delete them. + +**Note:** Removing partitions means you are removing your data, make backups BEFORE making new partitions. + +### Delete Existing Partitions + +1. **Delete All Partitions** (except `sda`): + - Select each partition and delete it, but be careful not to delete any partitions on `sda`. +2. **Expand Fedora Linux for x86_64**: + - Click the `>` indicator next to Fedora Linux to expand the default layout. + - You will see default partitions such as: + - `/boot/efi` (600 MB) + - `/boot` (1.2 GB) + - `/` (rest of the drive) + + +### Reclaim Space from Root Partition + +1. **Delete the Root Partition**: + - Select `/` and click the `-` button to delete it. + - Choose to delete all filesystems used by Fedora. + +### Define New Partitions + +1. **EFI System Partition** (Existing): + - Identify the **EFI System Partition** in the list (usually on the NVMe drive). + - Set the mount point to `/boot/efi`. + - Set the size to **600 MB**. + - Click **Update Settings**. + +2. **Create New Partitions**: + + - **/boot Partition**: + - Click `+` to add a new partition. + - Set the **Mount Point** to `/boot`. + - Set the size to **1.2 GB**. + - Click **Add mount point**. + + - **Swap Partition**: + - Click `+` to add a swap partition. + - Set **Device Type** to `Standard Partition`. + - Set the **Mount Point** to `swap`. + - Check the box to **Encrypt** the partition. + - Set the size as **RAM x 1.5** (e.g., for 32 GB of RAM, use 48 GB, use Google to help - RAM x 1.5 =). + - Click **Update Settings**. + + - **Root Partition (/)**: + - Click `+` to add the root partition. + - Set the **Mount Point** to `/`. + - Set the **Size** to `1000000` (Shortcut. Using a large number ensures it will use the rest of the drive). + - Click **Modify** and check the box to **Encrypt** the partition. + - Click **Save**. + + +[![New Partition Layout](https://raw.githubusercontent.com/FrameworkComputer/linux-docs/refs/heads/main/hibernation/images/2.png)](https://raw.githubusercontent.com/FrameworkComputer/linux-docs/refs/heads/main/hibernation/images/2.png) + +### Finalize Partitions + +1. **Confirm Encryption Settings**: + - After configuring the partitions, ensure that the device type shows `-` for encrypted partitions. + - This can later be verified with `lsblk -o NAME,FSTYPE,MOUNTPOINT`. + +2. **Finish Setup**: + - Click **Done** upper left corner. + - Set an **encryption password** when prompted. + - Accept the **Summary of Changes** to apply the partition layout. + + + +[![Enter Desired LUKS encryption passphrase](https://raw.githubusercontent.com/FrameworkComputer/linux-docs/refs/heads/main/hibernation/images/3.png)](https://raw.githubusercontent.com/FrameworkComputer/linux-docs/refs/heads/main/hibernation/images/3.png) + + + +### Finish your install process + + +[![Begin Installation](https://raw.githubusercontent.com/FrameworkComputer/linux-docs/refs/heads/main/hibernation/images/installrun3.png)](https://raw.githubusercontent.com/FrameworkComputer/linux-docs/refs/heads/main/hibernation/images/installrun3.png) + +1. **Click on Begin installation.** + +[![Installation Continues](https://raw.githubusercontent.com/FrameworkComputer/linux-docs/refs/heads/main/hibernation/images/installrun4.png)](https://raw.githubusercontent.com/FrameworkComputer/linux-docs/refs/heads/main/hibernation/images/installrun4.png) + +----------------------------------------------- + +## Lid Close and Hibernate Settings Installation and Usage Guide + + +This guide provides step-by-step instructions to install and use the **Lid Close and Hibernate Settings** application, allowing you to configure lid-close actions, hibernation settings, and manage GNOME extensions on your system. + +--- + +### Installation Steps + +1. **Download the RPM Package** + [Download the latest RPM](https://github.com/FrameworkComputer/suspend-then-hibernate-settings/releases/tag/Python) + _Recommended that you open in a new tab as not to lose this page._ + +3. **Install Using GNOME Software Center** + Double-click the downloaded RPM file. This will open the GNOME Software Center for installation. + Follow the prompts in the Software Center to complete the installation. + +[![Install Using GNOME Software Center](https://raw.githubusercontent.com/FrameworkComputer/linux-docs/refs/heads/main/hibernation/images/software-center.png)](https://raw.githubusercontent.com/FrameworkComputer/linux-docs/refs/heads/main/hibernation/images/software-center.png) + + +--- + +### Using Lid Close and Hibernate Settings + +**NOTE:** If you suspend or hibernate with Bluetooth enabled and happen to be on kernel 6.11 or greater, Please use this [workaround](https://github.com/FrameworkComputer/linux-docs/blob/main/hibernation/kernel-6-11-workarounds/suspend-hibernate-bluetooth-workaround.md#workaround-for-suspendhibernate-black-screen-on-resume-kernel-611) (open in a new tab), then return to this step. + +After installation, you can launch the application from your applications menu. + +[![Launch from applications menu](https://raw.githubusercontent.com/FrameworkComputer/linux-docs/refs/heads/main/hibernation/images/installed1.png)](https://raw.githubusercontent.com/FrameworkComputer/linux-docs/refs/heads/main/hibernation/images/installed1.png) + +### Available Features and Buttons + +Each button in the **Lid Close and Hibernate Settings** application provides specific functions for managing your system’s suspend and hibernate settings. + +[![Lid Close and Hibernate Settings](https://raw.githubusercontent.com/FrameworkComputer/linux-docs/refs/heads/main/hibernation/images/running1.png)](https://raw.githubusercontent.com/FrameworkComputer/linux-docs/refs/heads/main/hibernation/images/running1.png) + + +1. **Install Dependencies** + (**NOTE:** This is already installed, but this is offered just in case and as the software progresses, new features will be added.) + Click **Install Dependencies** to ensure all necessary packages are installed. This will check and install any missing dependencies for hibernation functionality. + +3. **Configure Hibernate** + Press **Configure Hibernate** to set up hibernation on your system. This button will handle configuration settings needed for hibernation support. + +4. **Manage Hibernate Extension** + - **Step 1**: Click **Manage Hibernate Extension** and choose **Install Extension** to add the Hibernate Status extension to GNOME. + - **Step 2**: After installation, **reboot your system**. + - **Step 3**: Launch **Lid Close and Hibernate Settings** _again_ (not a typo, just a bug), click **Manage Hibernate Extension**, and choose **Install Extension** once more. + This double installation process ensures the extension is fully integrated into your GNOME desktop. + NOTE: This a forked version of the gnome-shell-extension-hibernate-status GNOME extension. It is [hosted at this GitHub account](https://github.com/ctsdownloads/gnome-shell-extension-hibernate-status?tab=readme-ov-file#gnome-shell-extension-hibernate-status) + while the application here is in beta and was forked to remove functions that were not needed for this process and to meet compatibility with GNOME 47 - at the time of the fork, it stopped at GNOME 46, which this fork addressed for 47. + +[![Manage Hibernate Extension](https://raw.githubusercontent.com/FrameworkComputer/linux-docs/refs/heads/main/hibernation/images/hibernate-extension.png)](https://raw.githubusercontent.com/FrameworkComputer/linux-docs/refs/heads/main/hibernation/images/hibernate-extension.png) + + + +5. **(Optional) Set Suspend-then-Hibernate Time** + Use this button to set a custom delay time in seconds for your system to transition from suspend to hibernate mode. + +6. **(Optional) Set Lid Close Action** + Configure the action (Suspend or Hibernate) that occurs when your laptop lid is closed. + +7. **(Optional) Check Hibernation Settings** + Press this button to review your current hibernation settings, dependency status, and the state of the Hibernate extension. + +--- + +**NOTE:** No system reboot is required after using the application, except when prompted as part of the extension installation process. + + + diff --git a/hibernation/hibernate-fedora-manual-method.md b/hibernation/hibernate-fedora-manual-method.md new file mode 100644 index 0000000..b333776 --- /dev/null +++ b/hibernation/hibernate-fedora-manual-method.md @@ -0,0 +1,162 @@ + +# Manual Guide: Configuring Lid Close and Hibernate Settings on Linux (NOT Fedora official) + +This guide provides manual steps to configure lid-close and hibernate settings, install dependencies, and manage a GNOME extension for hibernation control. + +> What is hibernate? [Learn more here.](https://knowledgebase.frame.work/en_us/hibernation-on-linux-BkL1N5ffJg) + +**IMPORTANT:** This assumes you set up your partitions per our [instructions](https://github.com/FrameworkComputer/linux-docs/blob/main/hibernation/hibernate-fedora-automatic.md#access-partition-layout), first! + +**NOTE:** - [Secure boot needs to be disabled](https://github.com/FrameworkComputer/linux-docs/blob/main/misc/secure-boot.md#secure-boot-explained) (<--This affects EFI updaters for example + +**NOTE:** If you feel strongly about using btrfs subvolumes, other approaches that are untested by us, below are some links for community guides on that front: + +- [GUIDE] Framework 16 Hibernate (w/ swapfile) Setup on Fedora 40: [Read the full guide here](https://community.frame.work/t/guide-framework-16-hibernate-w-swapfile-setup-on-fedora-40/53080/1) +- [Guide] Fedora 36+: Hibernation with enabled secure boot and full disk encryption (FDE) decrypting over TPM2: [Read the guide here](https://community.frame.work/t/guide-fedora-36-hibernation-with-enabled-secure-boot-and-full-disk-encryption-fde-decrypting-over-tpm2/25474) +- Subvolume Btrfs Hibernate Approach: [Read more about this approach here](https://terminal.space/tech/hibernating-is-easy-now/). + +Otherwise, the manual method of doing this is outlined below: [ Prefer an app? See the automatic method here](https://github.com/FrameworkComputer/linux-docs/blob/main/hibernation/hibernate-fedora-automatic.md#fedora-41-hibernation-option-not-fedora-official-beta). + +--- + +## 1. Configure Hibernate using a swap partition + +### Step 1: Create `/etc/systemd/sleep.conf` (if it doesn't exist) + ```bash + sudo touch /etc/systemd/sleep.conf + ``` + +### Step 2: Install Additional Packages (if needed for hibernation) + ```bash + sudo dnf install -y audit policycoreutils-python-utils libnotify + ``` + +### Step 3: Set Hibernate Parameters in `sleep.conf` (Optional) + - Edit `/etc/systemd/sleep.conf` directly to add your desired hibernation settings without duplicating entries: + ```ini + [Sleep] + HibernateDelaySec=600 # Adjust delay time in seconds + ``` + +--- + +## 2. Manage GNOME Extension + +### Install Extension + +1. **Download the Hibernate Extension**: + ```bash + wget https://github.com/ctsdownloads/gnome-shell-extension-hibernate-status/archive/refs/heads/master.zip -O hibernate-extension.zip + ``` + +2. **Unzip the Extension**: + ```bash + unzip hibernate-extension.zip -d ~/gnome-shell-extension-hibernate-status + ``` + +3. **Install the Extension**: + - Locate the `metadata.json` file in the unzipped folder and find the UUID (`hibernate-status@ctsdownloads`). + - Move the extension to the GNOME extensions directory: + ```bash + mkdir -p ~/.local/share/gnome-shell/extensions/hibernate-status@ctsdownloads + cp -r ~/gnome-shell-extension-hibernate-status/hibernate-status@ctsdownloads/* ~/.local/share/gnome-shell/extensions/hibernate-status@ctsdownloads + ``` + +4. **Enable the Extension**: + ```bash + gnome-extensions enable hibernate-status@ctsdownloads + ``` + +5. **Restart GNOME Shell** (Press **Alt+F2**, type `r`, and press **Enter**; this may not be available on Wayland, so a reboot may be required). + +### Uninstall Extension + +1. **Disable and Remove the Extension**: + ```bash + gnome-extensions disable hibernate-status@ctsdownloads + rm -rf ~/.local/share/gnome-shell/extensions/hibernate-status@ctsdownloads + ``` + +--- + +## 3. Set Suspend-then-Hibernate Time (Optional) + +1. **Edit `sleep.conf` directly** to prevent duplicate entries: + ```bash + sudo nano /etc/systemd/sleep.conf + ``` + Add or modify the following entry: + ```ini + [Sleep] + HibernateDelaySec=600 # Adjust the delay time in seconds + ``` + +--- + +## 4. Set Lid Close Action (Optional) + +1. **Edit `logind.conf` manually**: + ```bash + sudo nano /etc/systemd/logind.conf + ``` + Add or modify the following entry: + ```ini + [Login] + HandleLidSwitch=suspend # Replace `suspend` with `hibernate` if desired + ``` + +--- + +## 5. Check Hibernation Settings + +1. **Check Dependency Installation**: + ```bash + rpm -q python3-gobject polkit + ``` + +2. **Check GNOME Extension Installation**: + ```bash + gnome-extensions list | grep hibernate-status + ``` + +3. **Verify Suspend-then-Hibernate Time**: + Check `/etc/systemd/sleep.conf` for the `HibernateDelaySec` entry. + +4. **Verify Lid Close Action**: + Check `/etc/systemd/logind.conf` for the `HandleLidSwitch` entry. + +--- + +## 6. Address SELinux Blocks on Hibernation + +If SELinux is preventing hibernation, you can use the following commands to create and apply a custom policy: + +1. **Temporarily Set SELinux to Permissive Mode** (Optional, for testing): + ```bash + sudo setenforce 0 + ``` + +2. **Generate a Custom SELinux Policy for Hibernation**: + ```bash + sudo ausearch -m avc -ts recent | audit2allow -M hibernate_policy + ``` + +3. **Apply the New SELinux Policy**: + ```bash + sudo semodule -i hibernate_policy.pp + ``` + +4. **Re-enable SELinux Enforcing Mode** (if previously set to permissive): + ```bash + sudo setenforce 1 + ``` + + Ensure `audit2allow` is installed by running: + ```bash + sudo dnf install -y policycoreutils-python-utils + ``` + +--- + +### Note +Please reboot your system after making changes to ensure all configurations take effect. diff --git a/hibernation/images/1.png b/hibernation/images/1.png new file mode 100644 index 0000000..4092b55 Binary files /dev/null and b/hibernation/images/1.png differ diff --git a/hibernation/images/2.png b/hibernation/images/2.png new file mode 100644 index 0000000..01cc106 Binary files /dev/null and b/hibernation/images/2.png differ diff --git a/hibernation/images/3.png b/hibernation/images/3.png new file mode 100644 index 0000000..d38f9c6 Binary files /dev/null and b/hibernation/images/3.png differ diff --git a/hibernation/images/hibernate-extension.png b/hibernation/images/hibernate-extension.png new file mode 100644 index 0000000..cee1462 Binary files /dev/null and b/hibernation/images/hibernate-extension.png differ diff --git a/hibernation/images/installed1.png b/hibernation/images/installed1.png new file mode 100644 index 0000000..984a143 Binary files /dev/null and b/hibernation/images/installed1.png differ diff --git a/hibernation/images/installrun3.png b/hibernation/images/installrun3.png new file mode 100644 index 0000000..8f0a5ab Binary files /dev/null and b/hibernation/images/installrun3.png differ diff --git a/hibernation/images/installrun4.png b/hibernation/images/installrun4.png new file mode 100644 index 0000000..8ee2f86 Binary files /dev/null and b/hibernation/images/installrun4.png differ diff --git a/hibernation/images/readme b/hibernation/images/readme new file mode 100644 index 0000000..4f27405 --- /dev/null +++ b/hibernation/images/readme @@ -0,0 +1 @@ +Directory for images. diff --git a/hibernation/images/running1.png b/hibernation/images/running1.png new file mode 100644 index 0000000..2359a47 Binary files /dev/null and b/hibernation/images/running1.png differ diff --git a/hibernation/images/software-center.png b/hibernation/images/software-center.png new file mode 100644 index 0000000..71ac5b2 Binary files /dev/null and b/hibernation/images/software-center.png differ diff --git a/hibernation/kernel-6-11-workarounds/rfkill-suspender.sh b/hibernation/kernel-6-11-workarounds/rfkill-suspender.sh new file mode 100644 index 0000000..1229800 --- /dev/null +++ b/hibernation/kernel-6-11-workarounds/rfkill-suspender.sh @@ -0,0 +1,61 @@ +#!/bin/bash + +# Define service files and paths +SUSPEND_SERVICE="/etc/systemd/system/bluetooth-rfkill-suspend.service" +RESUME_SERVICE="/etc/systemd/system/bluetooth-rfkill-resume.service" + +# Create and configure the suspend service +echo "Setting up bluetooth-rfkill-suspend.service..." +sudo tee "$SUSPEND_SERVICE" > /dev/null < /dev/null < "$recommendations_file" +> "$error_context_file" +> "$error_codes_file" +> "$state_changes_file" +> "$summary_file" +> "$focused_summary_file" + +# Variables for context-aware analysis +previous_line="" +error_burst_window=30 # seconds +declare -A error_timestamps +declare -A device_states + +# ANSI escape codes for text formatting +BOLD='\033[1m' +RED='\033[0;31m' +YELLOW='\033[1;33m' +GREEN='\033[0;32m' +BLUE='\033[0;34m' +CYAN='\033[0;36m' +RESET='\033[0m' + +# Enhanced noise filter - filters out normal system operations that aren't actual problems +is_harmless_system_noise() { + local line="$1" + + # === SECURITY/AUDIT NOISE (Normal system operations) === + [[ $line == *"audit"* && $line == *"CRED_ACQ"* ]] || # Normal credential acquisition + [[ $line == *"audit"* && $line == *"CRED_DISP"* ]] || # Normal credential disposal + [[ $line == *"audit"* && $line == *"USER_AUTH"* ]] || # Normal user authentication + [[ $line == *"audit"* && $line == *"USER_ACCT"* ]] || # Normal user account access + [[ $line == *"audit"* && $line == *"SESSION_OPEN"* ]] || # Normal session opening + [[ $line == *"audit"* && $line == *"SESSION_CLOSE"* ]] || # Normal session closing + + # === DEVICE MANAGEMENT NOISE (Normal udev operations) === + [[ $line == *"udev-worker"* && $line == *"BAT"* && $line == *"chmod"* ]] || # Battery permission setup + [[ $line == *"udev-worker"* && $line == *"ADP"* && $line == *"chmod"* ]] || # Power adapter permission setup + [[ $line == *"udev-worker"* && $line == *"/sys/"* && $line == *"chmod"* ]] || # General device permissions + [[ $line == *"udev-worker"* && $line == *"Process"* && $line == *"'/bin/chmod"* ]] || # Any chmod operations + + # === SYSTEMD SERVICE NOISE (Normal service management) === + [[ $line == *"systemd"* && $line == *"Starting"* ]] || # Service starting (normal) + [[ $line == *"systemd"* && $line == *"Started"* ]] || # Service started (normal) + [[ $line == *"systemd"* && $line == *"Stopping"* ]] || # Service stopping (normal) + [[ $line == *"systemd"* && $line == *"Stopped"* ]] || # Service stopped (normal) + [[ $line == *"systemd"* && $line == *"Reloading"* ]] || # Service reloading (normal) + [[ $line == *"systemd"* && $line == *"Reloaded"* ]] || # Service reloaded (normal) + [[ $line == *"systemd"* && $line == *"Deactivated successfully"* ]] || # Normal deactivation + + # === GNOME/DESKTOP NOISE === + [[ $line == *"gnome-shell"* ]] || # All gnome-shell messages (usually UI glitches) + [[ $line == *"org.gnome"* ]] || # GNOME application messages + [[ $line == *"gvfsd"* ]] || # GNOME virtual filesystem + + # === FRAMEWORK-SPECIFIC HARMLESS NOISE === + [[ $line == *"platform regulatory.0: Direct firmware load for regulatory.db failed"* ]] || + [[ $line == *"cros_ec_lpcs"* && $line == *"EC communication failed"* ]] || + [[ $line == *"ACPI BIOS Error"* && $line == *"AE_NOT_FOUND"* ]] || + [[ $line == *"intel_pstate"* && $line == *"Unknown P-state control mode"* ]] || + [[ $line == *"bluetooth hci0: Direct firmware load"* && $line == *"failed"* ]] || + [[ $line == *"rtw89"* && $line == *"firmware"* && $line == *"not found"* ]] || + [[ $line == *"iwlwifi"* && $line == *"api flags index"* && $line == *"larger than supported"* ]] || + [[ $line == *"ACPI"* && $line == *"_OSC failed"* && $line == *"not supported"* ]] || + [[ $line == *"platform efi-framebuffer.0: Cannot reserve"* && $line == *"resource"* ]] || + [[ $line == *"ucsi_ccg"* && $line == *"failed to reset PPM"* ]] || + [[ $line == *"ucsi_ccg"* && $line == *"PPM init failed"* ]] || + [[ $line == *"thunderbolt"* && $line == *"device disappeared"* ]] || + [[ $line == *"pcieport"* && $line == *"AER: Corrected error"* ]] || + [[ $line == *"pcieport"* && $line == *"PCIe Bus Error"* && $line == *"Corrected"* ]] || + [[ $line == *"ACPI Warning"* && $line == *"SystemIO range"* ]] || + [[ $line == *"ACPI Warning"* && $line == *"0x0000000000000400-0x000000000000041f"* ]] || + [[ $line == *"pci"* && $line == *"BAR"* && $line == *"bogus alignment"* ]] || + [[ $line == *"amd_pmc"* && $line == *"SMU debugging info"* ]] || + [[ $line == *"amdgpu"* && $line == *"WARN"* && $line == *"SMU feature is not enabled"* ]] || + [[ $line == *"mt7925e"* && $line == *"Message 00000010 (seq 1) timeout"* ]] || + [[ $line == *"rfkill"* && $line == *"input handler disabled"* ]] || + + # === NETWORK MANAGER NOISE (Normal network operations) === + [[ $line == *"NetworkManager"* && $line == *"policy"* ]] || # Normal network policy + [[ $line == *"wpa_supplicant"* && $line == *"Authentication"* ]] || # Normal WiFi auth + + # === POWER MANAGEMENT NOISE === + [[ $line == *"power_supply"* && $line == *"BAT"* ]] || # Normal battery events + [[ $line == *"power_supply"* && $line == *"ADP"* ]] || # Normal adapter events + + # === TEMPORARY FILE SYSTEM NOISE === + [[ $line == *"tmpfiles"* ]] || # Temporary file management + + # === NORMAL KERNEL INFORMATIONAL MESSAGES === + [[ $line == *"kernel:"* && $line == *"Bluetooth:"* && $line == *"hci"* ]] || # Bluetooth info + [[ $line == *"kernel:"* && $line == *"usb"* && $line == *"new"* ]] || # New USB device (normal) + [[ $line == *"kernel:"* && $line == *"usb"* && $line == *"disconnect"* ]] && # USB disconnect (normal) + return 0 + return 1 +} + +# Extract actual hardware details for context +get_device_details() { + echo "Hardware Context:" >> "$output_file" + + # Get actual GPU info + local gpu_info=$(lspci | grep -E "VGA|3D|Display") + if [ -n "$gpu_info" ]; then + echo " GPU: $gpu_info" >> "$output_file" + fi + + # Get NVMe/Storage info + local nvme_info=$(lspci | grep -i "non-volatile\|nvme") + if [ -n "$nvme_info" ]; then + echo " Storage: $nvme_info" >> "$output_file" + # Get NVMe model names + for nvme in /dev/nvme*n1; do + if [ -e "$nvme" ]; then + local model=$(sudo nvme id-ctrl "$nvme" 2>/dev/null | grep "mn " | awk '{print $2}' | xargs) + if [ -n "$model" ]; then + echo " $(basename $nvme): $model" >> "$output_file" + fi + fi + done + fi + + # Get WiFi card info + local wifi_info=$(lspci | grep -iE "wireless|wifi|802\.11|network controller.*wi-fi|network controller.*MT79|network controller.*intel|network controller.*realtek|network controller.*broadcom|network controller.*mediatek") + if [ -n "$wifi_info" ]; then + echo " WiFi: $wifi_info" >> "$output_file" + fi + + # Get RAM info + if command -v dmidecode >/dev/null 2>&1; then + # Get total RAM size - dmidecode shows size in MB for each module + local total_ram_mb=$(sudo dmidecode -t memory 2>/dev/null | grep "Size:" | grep -v "No Module Installed" | grep -v "Not Specified" | grep -E "[0-9]+ MB|[0-9]+ GB" | awk ' + BEGIN { sum = 0 } + /MB/ { sum += $2 } + /GB/ { sum += ($2 * 1024) } + END { if (sum > 0) print sum }') + + local ram_speed=$(sudo dmidecode -t memory 2>/dev/null | grep "Configured Memory Speed:" | head -1 | awk '{print $4 " " $5}') + local ram_type=$(sudo dmidecode -t memory 2>/dev/null | grep "Type:" | grep -v "Unknown" | grep -v "Error Correction Type" | head -1 | awk '{print $2}') + + if [ -n "$total_ram_mb" ] && [ "$total_ram_mb" -gt 0 ]; then + local ram_gb=$((total_ram_mb / 1024)) + local ram_output=" RAM: ${ram_gb} GB" + if [ -n "$ram_type" ] && [ "$ram_type" != "" ]; then + ram_output="$ram_output $ram_type" + fi + if [ -n "$ram_speed" ] && [ "$ram_speed" != " " ]; then + ram_output="$ram_output @ $ram_speed" + fi + echo "$ram_output" >> "$output_file" + fi + else + # Fallback to /proc/meminfo + local total_ram_kb=$(grep MemTotal /proc/meminfo 2>/dev/null | awk '{print $2}') + if [ -n "$total_ram_kb" ]; then + local total_ram_gb=$((total_ram_kb / 1024 / 1024)) + echo " RAM: ${total_ram_gb} GB (from /proc/meminfo)" >> "$output_file" + fi + fi + + echo "" >> "$output_file" +} + +# Detect AMD CPU generation for appropriate thermal thresholds +detect_amd_generation() { + # Use the already detected product/model information instead of parsing lscpu again + if [[ $PRODUCT_NAME =~ "Laptop 13" ]] || [[ $FRAMEWORK_MODEL =~ "Framework Laptop 13" ]]; then + # Framework Laptop 13 uses modern AMD (7040+ series or AI 300) + echo "modern" + elif [[ $PRODUCT_NAME =~ "Laptop 16" ]] || [[ $FRAMEWORK_MODEL =~ "Framework Laptop 16" ]]; then + # Framework Laptop 16 uses modern AMD (7040 series) + echo "modern" + elif [[ $PRODUCT_NAME =~ "Laptop 12" ]] || [[ $FRAMEWORK_MODEL =~ "Framework Laptop 12" ]]; then + # Framework Laptop 12 uses Intel, but check if AMD variant exists + echo "modern" + elif [[ $PRODUCT_NAME =~ "Desktop" ]] || [[ $FRAMEWORK_MODEL =~ "Framework Desktop" ]]; then + # Framework Desktop uses modern AMD (AI Max 300 series) + echo "modern" + else + # Fallback to lscpu parsing for non-Framework devices + local cpu_model=$(lscpu | grep "Model name" | awk '{print substr($0, index($0,$3))}') + if [[ $cpu_model =~ 7[4-9][0-9][0-9] ]] || [[ $cpu_model =~ 8[0-9][0-9][0-9] ]] || [[ $cpu_model =~ "AI " ]]; then + echo "modern" + else + echo "legacy" + fi + fi +} + +# Function to check real-time network connectivity +check_network_connectivity() { + echo " Network Connectivity:" >> "$output_file" + + # Check if any network interfaces are up + local interfaces_up=$(ip link show | grep "state UP" | wc -l) + + # Test actual internet connectivity + local internet_working=false + if ping -c 1 -W 2 8.8.8.8 >/dev/null 2>&1; then + internet_working=true + echo " Internet: ✅ Connected" >> "$output_file" + elif ping -c 1 -W 2 1.1.1.1 >/dev/null 2>&1; then + internet_working=true + echo " Internet: ✅ Connected" >> "$output_file" + else + echo " Internet: ❌ Not connected" >> "$output_file" + echo "IMPORTANT|NETWORK_CONNECTIVITY|Your internet is not working right now → Check your WiFi connection, network cable, or contact your internet provider" >> "$recommendations_file" + fi + + # Check WiFi status if available + local wifi_connected=false + if command -v iwconfig >/dev/null 2>&1; then + local wifi_count=$(iwconfig 2>/dev/null | grep "ESSID" | grep -v "off/any" | wc -l) + if [ "$wifi_count" -gt 0 ]; then + wifi_connected=true + local wifi_name=$(iwconfig 2>/dev/null | grep "ESSID" | grep -v "off/any" | head -1 | awk -F'"' '{print $2}') + echo " WiFi: ✅ Connected to \"$wifi_name\"" >> "$output_file" + else + echo " WiFi: ❌ Not connected" >> "$output_file" + if [ "$internet_working" = false ]; then + echo "IMPORTANT|WIFI_CONNECTIVITY|Your WiFi is not connected → Check that WiFi is enabled and you're connected to your network" >> "$recommendations_file" + fi + fi + fi + + # Check if ethernet is connected + local ethernet_connected=$(ip link show | grep -E "eth|enp|eno" | grep "state UP" | wc -l) + if [ "$ethernet_connected" -gt 0 ]; then + echo " Ethernet: ✅ Connected" >> "$output_file" + else + echo " Ethernet: ❌ Not connected" >> "$output_file" + fi + + # Set global variables for use in log analysis + NETWORK_CURRENTLY_WORKING="$internet_working" + WIFI_CURRENTLY_CONNECTED="$wifi_connected" + + echo "" >> "$output_file" +} +# Check current temperatures with Framework-specific thresholds +check_current_temperatures() { + if command -v sensors >/dev/null 2>&1; then + echo " Thermal Status:" >> "$output_file" + + # Get CPU temperature from multiple sources (priority order) + local cpu_temp="" + local cpu_source="" + + # 1. Try k10temp Tctl (AMD primary) + local tctl_temp=$(sensors 2>/dev/null | grep "Tctl:" | awk '{print $2}' | head -1 | sed 's/[+-]//g' | sed 's/°C//') + if [ -n "$tctl_temp" ]; then + cpu_temp="$tctl_temp" + cpu_source="Tctl" + else + # 2. Try Package temp (Intel primary) + local package_temp=$(sensors 2>/dev/null | grep "Package" | awk '{print $3}' | head -1 | sed 's/[+-]//g' | sed 's/°C//') + if [ -n "$package_temp" ]; then + cpu_temp="$package_temp" + cpu_source="Package" + else + # 3. Try Core 0 or cpu@4c (alternatives) + local core_temp=$(sensors 2>/dev/null | grep -E "Core.*0:|cpu@4c:" | awk '{print $3}' | head -1 | sed 's/[+-]//g' | sed 's/°C//') + if [ -n "$core_temp" ]; then + cpu_temp="$core_temp" + cpu_source="Core" + fi + fi + fi + + # Display all sensor output (condensed) + sensors 2>/dev/null | grep -E "Tctl|Package|edge|Composite|cpu@4c" >> "$output_file" + + # Apply temperature thresholds if we got a reading + if [ -n "$cpu_temp" ] && [[ $cpu_temp =~ ^[0-9]+\.?[0-9]*$ ]]; then + local temp_int=$(printf "%.0f" "$cpu_temp") + + # Detect CPU architecture for appropriate thresholds + local cpu_vendor=$(lscpu | grep "Vendor ID" | awk '{print $3}') + local is_framework_detected=false + + # Check if this is a Framework device + if [[ $PRODUCT_NAME =~ Framework ]] || [[ $PRODUCT_NAME =~ "Laptop 13" ]] || [[ $PRODUCT_NAME =~ "Laptop 16" ]] || [[ $PRODUCT_NAME =~ "Laptop 12" ]] || [[ $PRODUCT_NAME =~ "Desktop" ]]; then + is_framework_detected=true + fi + + # Apply Framework-specific AMD thresholds with generation detection + if [[ $cpu_vendor == "AuthenticAMD" ]] || [[ $cpu_vendor == "AMD" ]]; then + local amd_generation=$(detect_amd_generation) + + if [[ $amd_generation == "modern" ]]; then + # AMD 7040+ series - designed to run hotter safely + if [ "$temp_int" -ge 105 ]; then + echo "IMMEDIATE|THERMAL_EMERGENCY|Your processor is dangerously hot (${cpu_temp}°C) → Your laptop will shut down to protect itself. Stop using immediately, check that air vents aren't blocked, and let it cool down completely before using again" >> "$recommendations_file" + elif [ "$temp_int" -ge 100 ]; then + echo "URGENT|THERMAL_CRITICAL|Your processor is running very hot (${cpu_temp}°C) → This sustained high temperature could damage your laptop. Close demanding programs, ensure air vents are clear, and consider using a laptop cooling pad" >> "$recommendations_file" + elif [ "$temp_int" -ge 95 ]; then + echo "INFORMATIONAL|THERMAL_THROTTLING|Your processor is hot but this is normal (${cpu_temp}°C) → Modern AMD processors are designed to run at these temperatures under heavy load. Your laptop is automatically slowing down to stay cool" >> "$recommendations_file" + elif [ "$temp_int" -ge 90 ] && [ "$is_framework_detected" = true ]; then + echo "PREVENTIVE|THERMAL_ELEVATED|Your processor temperature is elevated (${cpu_temp}°C) → Consider reducing your workload or closing some programs if the laptop feels warm" >> "$recommendations_file" + fi + + # Temperature display for modern AMD + if [ "$is_framework_detected" = true ]; then + echo " Current CPU: ${cpu_temp}°C via ${cpu_source} (Modern AMD: runs hot by design - watch at 90°C, throttles at 95°C, critical at 100°C, emergency at 105°C)" >> "$output_file" + else + echo " Current CPU: ${cpu_temp}°C via ${cpu_source}" >> "$output_file" + fi + else + # Legacy AMD processors - more conservative thresholds + if [ "$temp_int" -ge 105 ]; then + echo "IMMEDIATE|THERMAL_EMERGENCY|Your processor is dangerously hot (${cpu_temp}°C) → Your laptop will shut down to protect itself. Stop using immediately, check that air vents aren't blocked, and let it cool down completely" >> "$recommendations_file" + elif [ "$temp_int" -ge 95 ]; then + echo "URGENT|THERMAL_CRITICAL|Your processor is too hot (${cpu_temp}°C) → This could damage your laptop. Close demanding programs immediately, check that air vents are clear, and reduce your workload" >> "$recommendations_file" + elif [ "$temp_int" -ge 90 ]; then + echo "IMPORTANT|THERMAL_WARNING|Your processor is running hot (${cpu_temp}°C) → This is too hot for older AMD processors. Close some programs and make sure air vents aren't blocked" >> "$recommendations_file" + elif [ "$temp_int" -ge 85 ] && [ "$is_framework_detected" = true ]; then + echo "PREVENTIVE|THERMAL_ELEVATED|Your processor temperature is elevated (${cpu_temp}°C) → Consider reducing your workload to keep temperatures lower" >> "$recommendations_file" + fi + + # Temperature display for legacy AMD + if [ "$is_framework_detected" = true ]; then + echo " Current CPU: ${cpu_temp}°C via ${cpu_source} (Older AMD: watch at 85°C, warning at 90°C, critical at 95°C, emergency at 105°C)" >> "$output_file" + else + echo " Current CPU: ${cpu_temp}°C via ${cpu_source}" >> "$output_file" + fi + fi + else + # Intel Framework thresholds - lower due to cooler operation + if [ "$temp_int" -ge 100 ]; then + echo "IMMEDIATE|THERMAL_EMERGENCY|Your processor is dangerously hot (${cpu_temp}°C) → Your laptop will shut down to protect itself. Stop using immediately, check that air vents aren't blocked, and let it cool down completely" >> "$recommendations_file" + elif [ "$temp_int" -ge 90 ]; then + echo "URGENT|THERMAL_CRITICAL|Your Intel processor is too hot (${cpu_temp}°C) → This could damage your laptop. Close demanding programs immediately, check that air vents are clear, and reduce your workload" >> "$recommendations_file" + elif [ "$temp_int" -ge 85 ]; then + echo "IMPORTANT|THERMAL_WARNING|Your Intel processor is running hot (${cpu_temp}°C) → Close some programs and make sure air vents aren't blocked. Intel processors should run cooler than this" >> "$recommendations_file" + elif [ "$temp_int" -ge 80 ] && [ "$is_framework_detected" = true ]; then + echo "PREVENTIVE|THERMAL_ELEVATED|Your Intel processor temperature is elevated (${cpu_temp}°C) → Consider reducing your workload to keep temperatures lower" >> "$recommendations_file" + fi + + if [ "$is_framework_detected" = true ]; then + echo " Current CPU: ${cpu_temp}°C via ${cpu_source} (Intel: watch at 80°C, warning at 85°C, critical at 90°C, emergency at 100°C)" >> "$output_file" + else + echo " Current CPU: ${cpu_temp}°C via ${cpu_source}" >> "$output_file" + fi + fi + + # Check GPU temperature if available + local gpu_temp=$(sensors 2>/dev/null | grep "edge:" | awk '{print $2}' | head -1 | sed 's/[+-]//g' | sed 's/°C//') + if [ -n "$gpu_temp" ] && [[ $gpu_temp =~ ^[0-9]+\.?[0-9]*$ ]]; then + local gpu_temp_int=$(printf "%.0f" "$gpu_temp") + if [ "$gpu_temp_int" -ge 95 ]; then + echo "URGENT|GPU_THERMAL|Your graphics card is dangerously hot (${gpu_temp}°C) → Close games, video editing software, or other graphics-intensive programs immediately to prevent damage" >> "$recommendations_file" + elif [ "$gpu_temp_int" -ge 85 ]; then + echo "IMPORTANT|GPU_THERMAL|Your graphics card is running hot (${gpu_temp}°C) → Consider reducing graphics settings in games or closing graphics-intensive programs" >> "$recommendations_file" + fi + echo " Current GPU: ${gpu_temp}°C (starts getting warm at 85°C, critical at 95°C)" >> "$output_file" + fi + + else + echo " Thermal sensors not accessible or no temperature reading available" >> "$output_file" + fi + fi +} + +# Parse error codes and frequencies +parse_error_codes() { + local line="$1" + # Extract structured error information + local error_code="" + + if [[ $line =~ error\ -([0-9]+) ]]; then + error_code="errno_${BASH_REMATCH[1]}" + elif [[ $line =~ errno=([0-9]+) ]]; then + error_code="errno_${BASH_REMATCH[1]}" + elif [[ $line =~ fault\ (0x[0-9a-f]+) ]]; then + error_code="fault_${BASH_REMATCH[1]}" + elif [[ $line =~ "I/O error" ]]; then + error_code="io_error" + elif [[ $line =~ "timeout" ]]; then + error_code="timeout" + elif [[ $line =~ "hang" ]]; then + error_code="hang" + fi + + if [ -n "$error_code" ]; then + echo "$error_code|$line" >> "$error_codes_file" + fi +} + +# Track device state changes +track_state_changes() { + local line="$1" + local timestamp="$2" + + # USB device state changes + if [[ $line =~ "USB disconnect, address ([0-9]+)" ]]; then + device_states["usb_${BASH_REMATCH[1]}"]="disconnected:$timestamp" + echo "USB_DISCONNECT|$timestamp|$line" >> "$state_changes_file" + elif [[ $line =~ "new.*USB device.*address ([0-9]+)" ]]; then + local addr="${BASH_REMATCH[1]}" + if [[ -n "${device_states["usb_$addr"]}" ]]; then + echo "USB_RECONNECT|$timestamp|$line" >> "$state_changes_file" + else + echo "USB_CONNECT|$timestamp|$line" >> "$state_changes_file" + fi + device_states["usb_$addr"]="connected:$timestamp" + fi + + # Thermal state changes + if [[ $line =~ "thermal.*throttling" ]]; then + device_states["thermal"]="throttling:$timestamp" + echo "THERMAL_THROTTLE|$timestamp|$line" >> "$state_changes_file" + elif [[ $line =~ "thermal.*normal" ]]; then + if [[ "${device_states["thermal"]}" =~ throttling ]]; then + echo "THERMAL_RECOVERY|$timestamp|$line" >> "$state_changes_file" + fi + device_states["thermal"]="normal:$timestamp" + fi + + # GPU state changes + if [[ $line =~ "amdgpu.*ring.*timeout" ]]; then + device_states["gpu"]="hang:$timestamp" + echo "GPU_HANG|$timestamp|$line" >> "$state_changes_file" + elif [[ $line =~ "amdgpu.*ring.*recovered" ]]; then + if [[ "${device_states["gpu"]}" =~ hang ]]; then + echo "GPU_RECOVERY|$timestamp|$line" >> "$state_changes_file" + fi + device_states["gpu"]="recovered:$timestamp" + fi +} + +# Context-aware error analysis +analyze_error_context() { + local current_line="$1" + local timestamp="$2" + + # GPU hang sequences + if [[ $current_line =~ "amdgpu.*timeout" ]] && [[ $previous_line =~ "amdgpu.*ring" ]]; then + echo "GPU_HANG_SEQUENCE|$timestamp|Ring timeout followed by GPU timeout" >> "$error_context_file" + return 0 + fi + + # NVMe controller reset sequences + if [[ $current_line =~ "nvme.*reset controller" ]] && [[ $previous_line =~ "nvme.*I/O.*timeout" ]]; then + echo "NVME_RESET_SEQUENCE|$timestamp|I/O timeout triggered controller reset" >> "$error_context_file" + return 0 + fi + + # USB enumeration failure patterns + if [[ $current_line =~ "device not accepting address" ]] && [[ $previous_line =~ "USB disconnect" ]]; then + echo "USB_ENUM_FAILURE|$timestamp|Disconnect followed by enumeration failure" >> "$error_context_file" + return 0 + fi + + # EC timeout patterns + if [[ $current_line =~ "cros_ec.*timeout" ]] && [[ $previous_line =~ "cros_ec.*command" ]]; then + echo "EC_COMMAND_TIMEOUT|$timestamp|EC command timeout sequence" >> "$error_context_file" + return 0 + fi + + return 1 +} + +# Enhanced function to translate technical errors into plain English +analyze_and_recommend() { + local log_line="$1" + + # Critical GPU hangs - model-specific charger recommendations with plain English + if [[ $log_line =~ amdgpu.*ring.*timeout|amdgpu.*job.*timeout|amdgpu.*GPU.*hang ]]; then + local charger_spec="" + local plain_english="Your computer's graphics stopped working and might have crashed. This usually happens when your charger isn't powerful enough, your graphics need updating, or your computer is too hot." + + if [[ $PRODUCT_NAME =~ "Laptop 13" ]] || [[ $FRAMEWORK_MODEL =~ "Framework Laptop 13" ]] || + [[ $PRODUCT_NAME =~ "Laptop 12" ]] || [[ $FRAMEWORK_MODEL =~ "Framework Laptop 12" ]]; then + charger_spec="verify you're using the official 60W charger" + plain_english="$plain_english Make sure you're using the official Framework 60W charger that came with your laptop (not a phone charger)." + elif [[ $PRODUCT_NAME =~ "Laptop 16" ]] || [[ $FRAMEWORK_MODEL =~ "Framework Laptop 16" ]]; then + charger_spec="verify you're using the official 180W charger" + plain_english="$plain_english Make sure you're using the official Framework 180W charger (the big one that came with your Laptop 16)." + elif [[ $PRODUCT_NAME =~ "Desktop" ]] || [[ $FRAMEWORK_MODEL =~ "Framework Desktop" ]]; then + charger_spec="check power supply connections" + plain_english="$plain_english Check that all power cables are plugged in properly to your desktop." + else + charger_spec="verify adequate power supply" + plain_english="$plain_english Check that your charger is working and plugged in properly." + fi + + echo "URGENT|GPU_HANG|Your computer's graphics crashed → $plain_english Also check for graphics updates and contact support if this keeps happening" >> "$recommendations_file" + return + fi + + # Additional serious hardware issues that cause crashes/freezes +if [[ $log_line =~ "i915.*GPU.*hang"|"i915.*reset"|"drm.*GPU.*hang"|"Machine check"|"mce:"|"MCE:"|"DMAR.*fault"|"IOMMU.*fault"|"DMA.*fault"|"CPU.*failed"|"processor.*failed"|"SMP.*failed" ]]; then + local issue_type="hardware problem" + if [[ $log_line =~ "i915"|"drm.*GPU" ]]; then + issue_type="Intel graphics crashed" + echo "URGENT|INTEL_GPU_HANG|$issue_type → Close graphics programs, restart, and check for driver updates" >> "$recommendations_file" + elif [[ $log_line =~ "Machine check"|"mce:" ]]; then + issue_type="serious hardware fault detected" + echo "IMMEDIATE|HARDWARE_ERROR|$issue_type → Your computer detected a hardware problem that could cause crashes. Contact support immediately" >> "$recommendations_file" + elif [[ $log_line =~ "DMAR"|"IOMMU"|"DMA.*fault" ]]; then + issue_type="hardware communication error" + echo "URGENT|DMA_FAULT|$issue_type → This can cause sudden freezes. Try disabling IOMMU/VT-d in BIOS settings or contact support" >> "$recommendations_file" + else + issue_type="processor core failure" + echo "IMMEDIATE|CPU_FAILURE|$issue_type → Hardware problem detected that can cause crashes. Contact support" >> "$recommendations_file" + fi + return +fi + + # Thermal emergencies with plain English + if [[ $log_line =~ critical.*temperature|thermal.*emergency|over.*temperature ]]; then + local thermal_explanation="Your computer got dangerously hot and will shut down to protect itself from damage. This means the cooling fans might be blocked with dust, you're running very demanding programs, or the computer needs cleaning." + echo "IMMEDIATE|THERMAL_CRITICAL|Computer dangerously hot → $thermal_explanation Stop using it immediately, let it cool down, and clean the air holes" >> "$recommendations_file" + return + fi + + # System freeze/lockup detection with plain English + if [[ $log_line =~ "hard LOCKUP"|"Kernel panic"|"kernel BUG"|"BUG: kernel NULL pointer dereference" ]]; then + local freeze_explanation="Your computer completely froze and stopped working. This usually means something serious went wrong with the software, a part is failing, or there's a problem with your memory. You'll need to force restart by holding the power button for 10 seconds." + echo "IMMEDIATE|SYSTEM_FREEZE|Computer completely frozen → $freeze_explanation If this keeps happening, contact support because a part might be broken" >> "$recommendations_file" + return + fi + + # Soft lockups and hung tasks with plain English + if [[ $log_line =~ "soft lockup"|"hung_task"|"blocked for more than.*seconds"|"task.*blocked for more than" ]]; then + local hang_explanation="Your computer is running very slowly or getting stuck. This means a program got stuck and won't respond, your hard drive is having trouble, or too many programs are running at once." + echo "URGENT|SOFT_LOCKUP|Computer running very slowly → $hang_explanation Try closing programs or restart if everything feels frozen" >> "$recommendations_file" + return + fi + + # RCU stalls (kernel responsiveness issues) with plain English + if [[ $log_line =~ "rcu_sched stall"|"rcu_preempt stall"|"RCU.*stall" ]]; then + local rcu_explanation="The main parts of your computer got stuck, which can make everything freeze or become extremely slow. This is serious and usually needs a restart to fix." + echo "URGENT|RCU_STALL|Main computer functions stuck → $rcu_explanation Restart your computer if it becomes unresponsive" >> "$recommendations_file" + return + fi + + # CPU stalls and scheduler issues with plain English + if [[ $log_line =~ "CPU.*stall"|"NMI watchdog"|"watchdog.*BUG"|"scheduling while atomic" ]]; then + local cpu_explanation="Your computer's main processor ran into a serious problem and might cause everything to freeze or become unstable. This could be from parts overheating, hardware problems, or software bugs." + echo "URGENT|CPU_STALL|Main processor problem → $cpu_explanation Watch for stability problems and contact support if issues continue" >> "$recommendations_file" + return + fi + + # Memory-related freezes with plain English + if [[ $log_line =~ "Out of memory"|"oom-killer"|"Killed process"|"page allocation failure"|"Memory allocation failed" ]]; then + local memory_explanation="Your computer ran out of memory and had to shut down programs to free up space. This can cause freezing if important programs get shut down. Close some programs or restart to free up memory." + echo "URGENT|MEMORY_CRITICAL|Computer out of memory → $memory_explanation Consider closing browser tabs, big programs, or getting more memory" >> "$recommendations_file" + return + fi + + # I/O freezes and storage issues with plain English + if [[ $log_line =~ "task.*in.*state.*for.*seconds"|"INFO: task.*blocked for more than.*seconds"|"blk_update_request.*I/O error" ]]; then + local io_explanation="Your computer's storage (where files are saved) is having trouble, which can make programs freeze when they try to open or save files. This could mean your hard drive is having problems or getting too hot." + echo "IMPORTANT|IO_FREEZE|File storage having problems → $io_explanation Check that your computer isn't too hot and consider backing up important files" >> "$recommendations_file" + return + fi + + # USB enumeration failures with plain English + if [[ $log_line =~ device.*not.*accepting.*address|device.*descriptor.*read.*error.*-110 ]]; then + local usb_explanation="A USB device (like an expansion card or accessory) couldn't connect properly. This often happens when connections are loose or USB ports have problems." + echo "IMPORTANT|USB_ENUM_FAIL|USB device couldn't connect → $usb_explanation Try unplugging and reconnecting the device, or try a different USB port" >> "$recommendations_file" + return + fi + + # NVMe issues with plain English + if [[ $log_line =~ nvme.*I/O.*timeout|nvme.*reset.*controller|nvme.*resetting.*controller ]]; then + local nvme_explanation="Your main storage drive is having trouble responding and might be failing or getting too hot. This can cause slow performance or you could lose files." + echo "URGENT|NVME_TIMEOUT|Main storage drive having problems → $nvme_explanation Back up important files immediately and check that the drive is properly connected" >> "$recommendations_file" + return + fi + + # WiFi firmware crashes with plain English + if [[ $log_line =~ iwlwifi.*firmware.*error|iwlwifi.*microcode.*SW.*error|mt7925.*firmware.*error|mt7925.*microcode.*error|mt7925.*firmware.*assert ]]; then + local wifi_explanation="Your WiFi card's software crashed, which will cause WiFi to disconnect and have connection problems. This is usually fixable with driver updates or diagnostic tools." + echo "IMPORTANT|WIFI_FIRMWARE|WiFi software crashed → $wifi_explanation Run the Enhanced WiFi Analyzer tool: https://github.com/FrameworkComputer/linux-docs/tree/main/Enhanced-WiFi-Analyzer" >> "$recommendations_file" + return + fi + + # WiFi connection drops (tracked for pattern analysis) + if [[ $log_line =~ wl[a-z0-9]*.*disconnected|wifi.*connection.*lost|deauthenticated|disassociated|CTRL-EVENT-DISCONNECTED ]]; then + echo "WIFI_DROP|$(date)|$log_line" >> "$state_changes_file" + return + fi + + # Battery and power issues with plain English + if [[ $log_line =~ "battery.*critical"|"battery.*low"|"power.*critical"|"AC.*disconnect" ]]; then + local power_explanation="Your computer's power system is having problems. This could be low battery, charger problems, or power management issues." + echo "IMPORTANT|POWER_ISSUE|Power system problems → $power_explanation Check that your charger is connected and working properly" >> "$recommendations_file" + return + fi + + # Network/Ethernet issues with plain English - but only if network is actually down + if [[ $log_line =~ "Network is unreachable"|"No route to host"|"Connection timed out"|"ethernet.*link.*down"|"network.*unreachable"|"soft blocked"|"hard blocked"|"rfkill.*block"|"No network connectivity"|"connection failed"|"link is not ready"|"disconnected"|"no carrier"|"network interface.*down" ]]; then + # Only report network issues if network is actually down right now + if [[ "$NETWORK_CURRENTLY_WORKING" != "true" ]]; then + local network_explanation="Your network connection (WiFi or cable) is having problems connecting or staying connected." + echo "IMPORTANT|NETWORK_ISSUE|Network connection problems → $network_explanation Check your network cables, WiFi signal strength, or router. If WiFi is blocked, check airplane mode or hardware WiFi switch" >> "$recommendations_file" + fi + return + fi + + # Audio/Sound issues with plain English + if [[ $log_line =~ "audio.*error"|"sound.*fail"|"alsa.*error"|"pulseaudio.*error" ]]; then + local audio_explanation="Your sound system is having problems, which might cause no sound, crackling, or audio errors." + echo "IMPORTANT|AUDIO_ISSUE|Sound system problems → $audio_explanation Try restarting sound services or check sound settings" >> "$recommendations_file" + return + fi + + # Filesystem errors with plain English + if [[ $log_line =~ "filesystem.*error"|"ext4.*error"|"btrfs.*error"|"corruption" ]]; then + local fs_explanation="Your file system found errors or corruption, which could cause you to lose files or make your computer unstable." + echo "URGENT|FILESYSTEM_ERROR|File system errors found → $fs_explanation Back up important files immediately and run file system checks" >> "$recommendations_file" + return + fi + + # CATCH-ALL: Only flag ACTUAL problems, not routine system operations + if [[ $log_line =~ error|Error|ERROR|warning|Warning|WARNING|fail|Fail|FAIL|critical|Critical|CRITICAL|fatal|Fatal|FATAL|panic|Panic|PANIC ]]; then + + # FIRST: Check if this is just normal system noise that contains error keywords + if is_harmless_system_noise "$log_line"; then + return # Ignore it completely + fi + + # SECOND: Check for specific patterns that are actually problems + local is_real_problem=false + local simple_explanation="" + + # Hardware failures that users should know about + if [[ $log_line =~ "Input/output error"|"I/O error"|"read error"|"write error" ]]; then + is_real_problem=true + simple_explanation="Your computer can't read or write files properly. Your hard drive might be breaking. Save your important files somewhere else right away." + elif [[ $log_line =~ "No space left on device"|"disk full" ]]; then + is_real_problem=true + simple_explanation="Your computer is completely full and can't save anything new. Delete some files or photos to free up space." + elif [[ $log_line =~ "Temperature above threshold"|"critical temperature" ]]; then + is_real_problem=true + simple_explanation="Your computer is getting too hot and might shut down to protect itself. Clean the air holes and close heavy programs." + elif [[ $log_line =~ "segmentation fault"|"segfault" ]]; then + is_real_problem=true + simple_explanation="A program just crashed. This happens sometimes, but if it keeps happening, something might be wrong." + elif [[ $log_line =~ "unable to mount"|"mount failed" ]]; then + is_real_problem=true + simple_explanation="Your computer can't see a connected drive or USB stick. Check that everything is plugged in properly." + elif [[ $log_line =~ "Network is unreachable"|"No route to host"|"Connection timed out"|"network is down"|"soft blocked"|"hard blocked"|"rfkill.*block"|"No network connectivity"|"connection failed"|"link is not ready"|"disconnected"|"no carrier"|"network interface.*down" ]]; then + is_real_problem=true + simple_explanation="Your internet isn't working. Check your WiFi connection or network cable." + elif [[ $log_line =~ "authentication failed"|"login failed"|"permission denied" ]] && [[ ! $log_line =~ "audit"|"systemd" ]]; then + is_real_problem=true + simple_explanation="Something couldn't log in or get permission to do what it needs. Some features might not work properly." + fi + + # Only report if it's actually a problem users should care about + if [[ $is_real_problem == true ]]; then + echo "IMPORTANT|ACTUAL_PROBLEM|$simple_explanation" >> "$recommendations_file" + fi + + # If we can't determine what it is, just ignore it rather than create noise + return + fi +} + +# Ensure necessary packages are installed based on the operating system +if [ -f /etc/os-release ]; then + OS_ID=$(grep ^ID= /etc/os-release | awk -F= '{print $2}' | tr -d '"') + OS_VERSION_ID=$(grep ^VERSION_ID= /etc/os-release | awk -F= '{print $2}' | tr -d '"') + + # Check and install required packages based on the distribution + case "$OS_ID" in + ubuntu) + sudo apt-get update -qq + sudo apt-get install -y -qq pciutils iw inxi lm-sensors bc || { echo -e "${BOLD}Package installation failed on Ubuntu.${RESET}"; exit 1; } + ;; + debian) + sudo apt-get update -qq + sudo apt-get install -y -qq pciutils iw inxi lm-sensors bc || { echo -e "${BOLD}Package installation failed on Debian.${RESET}"; exit 1; } + ;; + linuxmint) + sudo apt-get update -qq + sudo apt-get install -y -qq pciutils iw inxi lm-sensors bc || { echo -e "${BOLD}Package installation failed on Linux Mint.${RESET}"; exit 1; } + ;; + pop) + sudo apt-get update -qq + sudo apt-get install -y -qq pciutils iw inxi lm-sensors || { echo -e "${BOLD}Package installation failed on Pop!_OS.${RESET}"; exit 1; } + ;; + fedora) + sudo dnf install -y -q pciutils iw inxi lm_sensors || { echo -e "${BOLD}Package installation failed on Fedora.${RESET}"; exit 1; } + ;; + arch) + sudo pacman -Sy --needed --noconfirm pciutils iw inxi lm_sensors || { echo -e "${BOLD}Package installation failed on Arch Linux.${RESET}"; exit 1; } + ;; + manjaro) + sudo pacman -Sy --needed --noconfirm pciutils iw inxi lm_sensors || { echo -e "${BOLD}Package installation failed on Manjaro.${RESET}"; exit 1; } + ;; + endeavouros) + sudo pacman -Sy --needed --noconfirm pciutils iw inxi lm_sensors || { echo -e "${BOLD}Package installation failed on EndeavourOS.${RESET}"; exit 1; } + ;; + opensuse-tumbleweed|opensuse-leap|opensuse) + sudo zypper install -y pciutils iw inxi sensors || { echo -e "${BOLD}Package installation failed on openSUSE.${RESET}"; exit 1; } + ;; + nixos) + # NixOS uses declarative package management - check if tools are available + missing_tools=() + command -v lspci >/dev/null || missing_tools+=("pciutils") + command -v lsusb >/dev/null || missing_tools+=("usbutils") + command -v dmidecode >/dev/null || missing_tools+=("dmidecode") + command -v iw >/dev/null || missing_tools+=("iw") + command -v inxi >/dev/null || missing_tools+=("inxi") + command -v sensors >/dev/null || missing_tools+=("lm_sensors") + + if [ ${#missing_tools[@]} -gt 0 ]; then + echo -e "${BOLD}${YELLOW}Missing tools on NixOS: ${missing_tools[*]}${RESET}" + echo "Add these packages to your configuration.nix:" + echo "environment.systemPackages = with pkgs; [" + for tool in "${missing_tools[@]}"; do + echo " $tool" + done + echo "];" + echo "Then run: sudo nixos-rebuild switch" + echo "" + echo "Continuing with available tools..." + fi + ;; + bluefin|bazzite) + # Do not install any packages on these distributions + # Just skip installation. + ;; + *) + echo -e "${BOLD}Unsupported distribution: $OS_ID${RESET}" + echo "Supported distributions: Ubuntu, Debian, Linux Mint, Pop!_OS, Fedora, Arch Linux, Manjaro, EndeavourOS, openSUSE, NixOS" + exit 1 + ;; + esac +else + echo -e "${BOLD}Could not detect the OS distribution.${RESET}" + exit 1 +fi + +# Function to display a clean, terminal-friendly progress bar +show_progress_with_context() { + local percentage=$1 + local context="$2" + local width=40 + local filled=$((percentage * width / 100)) + local empty=$((width - filled)) + local bar="" + + # Check if terminal supports Unicode (UTF-8) + if [[ "$LANG" =~ UTF-8 ]] || [[ "$LC_ALL" =~ UTF-8 ]] || [[ "$LC_CTYPE" =~ UTF-8 ]]; then + # Use Unicode blocks for modern terminals + local fill_char="█" + local empty_char="░" + local arrow="▶" + else + # Fallback to ASCII for older/basic terminals + local fill_char="=" + local empty_char="-" + local arrow=">" + fi + + # Build the progress bar + for ((i=0; i> "$summary_file" + + # Analyze line for recommendations + analyze_and_recommend "$line" + + # Parse error codes for frequency analysis + parse_error_codes "$line" + + # Track device state changes + track_state_changes "$line" "$timestamp" + + # Analyze error context patterns + analyze_error_context "$line" "$timestamp" + previous_line="$line" + + # Only add to focused summary if it contains actual critical error patterns + if [[ $line =~ "timeout"|"fault"|"failed"|"error.*critical"|"hang"|"crash"|"corruption"|"hard LOCKUP"|"Kernel panic"|"soft lockup"|"hung_task"|"rcu.*stall"|"CPU.*stall"|"NMI watchdog"|"Out of memory"|"oom-killer"|"blocked for more than.*seconds" ]]; then + echo "$line" >> "$focused_summary_file" + fi +} + +# Function to get enhanced system information +get_system_info() { + echo "===== System Information =====" > "$output_file" + echo "" >> "$output_file" + echo "Kernel version: $(uname -r)" >> "$output_file" + + # Get desktop environment from the actual running session + local user_desktop="" + local user_session="" + + # Try multiple methods to get desktop environment + if [ -n "$SUDO_USER" ]; then + # Get desktop environment from systemctl + user_desktop=$(systemctl --user show-environment 2>/dev/null | grep "XDG_CURRENT_DESKTOP=" | awk -F= '{print $2}' 2>/dev/null) + user_session=$(systemctl --user show-environment 2>/dev/null | grep "XDG_SESSION_TYPE=" | awk -F= '{print $2}' 2>/dev/null) + + # Method 2: If that fails, try loginctl + if [ -z "$user_desktop" ]; then + local session_id=$(loginctl list-sessions --no-legend | grep "$SUDO_USER" | awk '{print $1}' | head -1) + if [ -n "$session_id" ]; then + user_desktop=$(loginctl show-session "$session_id" -p Desktop --value 2>/dev/null) + user_session=$(loginctl show-session "$session_id" -p Type --value 2>/dev/null) + fi + fi + + # Method 3: Check running processes as fallback + if [ -z "$user_desktop" ]; then + if pgrep -u "$SUDO_USER" gnome-shell >/dev/null 2>&1; then + user_desktop="GNOME" + elif pgrep -u "$SUDO_USER" kwin >/dev/null 2>&1; then + user_desktop="KDE" + elif pgrep -u "$SUDO_USER" xfce4-panel >/dev/null 2>&1; then + user_desktop="XFCE" + fi + + # Detect session type from processes + if pgrep -u "$SUDO_USER" "Xorg" >/dev/null 2>&1; then + user_session="x11" + elif pgrep -u "$SUDO_USER" "gnome-shell" >/dev/null 2>&1 && [ -z "$user_session" ]; then + user_session="wayland" + fi + fi + else + user_desktop="$XDG_CURRENT_DESKTOP" + user_session="$XDG_SESSION_TYPE" + fi + + # Display desktop environment info + if [ -n "$user_desktop" ]; then + if [ -n "$user_session" ]; then + echo "Desktop Environment: $user_desktop ($user_session)" >> "$output_file" + else + echo "Desktop Environment: $user_desktop" >> "$output_file" + fi + else + echo "Desktop Environment: Unknown" >> "$output_file" + fi + + # For distribution, read from /etc/os-release + if [ -f /etc/os-release ]; then + OS_NAME=$(grep PRETTY_NAME /etc/os-release | awk -F= '{print $2}' | tr -d '"') + echo "Distribution: $OS_NAME" >> "$output_file" + else + echo "Distribution: Unknown (no /etc/os-release)" >> "$output_file" + fi + + echo "BIOS Version: $(sudo dmidecode -s bios-version 2>/dev/null || echo 'Unknown')" >> "$output_file" + + # Enhanced Framework-specific information with model detection + echo "Hardware Information:" >> "$output_file" + PRODUCT_NAME=$(sudo dmidecode -s system-product-name 2>/dev/null || echo 'Unknown') + FRAMEWORK_MODEL=$(sudo dmidecode -s system-version 2>/dev/null || echo 'Unknown') + echo " Product: $PRODUCT_NAME" >> "$output_file" + + + + # Check if this is a Framework laptop with model-specific detection + # Framework products can have names like "Laptop 13 (AMD Ryzen AI 300 Series)", "Framework Laptop 13", "Framework Desktop", etc. + if [[ $PRODUCT_NAME =~ Framework ]] || [[ $PRODUCT_NAME =~ "Laptop 13" ]] || [[ $PRODUCT_NAME =~ "Laptop 16" ]] || [[ $PRODUCT_NAME =~ "Laptop 12" ]] || [[ $PRODUCT_NAME =~ "Desktop" ]]; then + echo " Framework device detected - applying Framework-specific diagnostics" >> "$output_file" + + # Model-specific information + if [[ $PRODUCT_NAME =~ "Laptop 13" ]] || [[ $FRAMEWORK_MODEL =~ "Framework Laptop 13" ]]; then + echo " Detected: Framework Laptop 13 - checking USB-C power delivery and thermal management" >> "$output_file" + elif [[ $PRODUCT_NAME =~ "Laptop 16" ]] || [[ $FRAMEWORK_MODEL =~ "Framework Laptop 16" ]]; then + echo " Detected: Framework Laptop 16 - checking GPU module and enhanced thermal envelope" >> "$output_file" + elif [[ $PRODUCT_NAME =~ "Laptop 12" ]] || [[ $FRAMEWORK_MODEL =~ "Framework Laptop 12" ]]; then + echo " Detected: Framework Laptop 12 - checking 2-in-1 convertible features and thermal management" >> "$output_file" + elif [[ $PRODUCT_NAME =~ "Desktop" ]] || [[ $FRAMEWORK_MODEL =~ "Framework Desktop" ]]; then + echo " Detected: Framework Desktop - checking Mini-ITX system and modular components" >> "$output_file" + fi + + # Expansion Cards Detection + echo " Expansion Cards:" >> "$output_file" + + # Check for HDMI and DisplayPort expansion cards when active + local hdmi_dp_cards=$(lsusb | grep -iE "HDMI|DisplayPort") + if [ -n "$hdmi_dp_cards" ]; then + echo "$hdmi_dp_cards" | while read -r line; do + echo " $line" >> "$output_file" + done + else + echo " No active HDMI/DisplayPort expansion cards detected" >> "$output_file" + fi + + # Power and Battery Status + echo " Power Status:" >> "$output_file" + + # Check if AC adapter is connected + local ac_connected="Unknown" + if [ -f /sys/class/power_supply/ADP*/online ]; then + local ac_status=$(cat /sys/class/power_supply/ADP*/online 2>/dev/null) + if [ "$ac_status" = "1" ]; then + ac_connected="Connected" + else + ac_connected="Disconnected" + fi + elif [ -f /sys/class/power_supply/AC*/online ]; then + local ac_status=$(cat /sys/class/power_supply/AC*/online 2>/dev/null) + if [ "$ac_status" = "1" ]; then + ac_connected="Connected" + else + ac_connected="Disconnected" + fi + fi + echo " AC Power: $ac_connected" >> "$output_file" + + # Battery status + if [ -f /sys/class/power_supply/BAT*/capacity ]; then + local battery_level=$(cat /sys/class/power_supply/BAT*/capacity 2>/dev/null | head -1) + local battery_status=$(cat /sys/class/power_supply/BAT*/status 2>/dev/null | head -1) + echo " Battery: ${battery_level}% (${battery_status})" >> "$output_file" + + # Battery health check using upower + if command -v upower >/dev/null 2>&1; then + local battery_health=$(upower -i $(upower -e | grep battery) 2>/dev/null | awk '/energy-full:/ {f=$2} /energy-full-design:/ {d=$2} END {h=(f/d)*100; print (h<85) ? "❌ Battery is NOT healthy" : "✅ Battery is healthy"}') + if [ -n "$battery_health" ]; then + echo " Health: $battery_health" >> "$output_file" + fi + fi + else + echo " Battery: Status unavailable" >> "$output_file" + fi + + # Get thermal information with intelligent thresholds + check_current_temperatures + + # Check real-time network connectivity + check_network_connectivity + fi + + # ALWAYS check Framework distro compatibility if any Framework detected + if [[ $PRODUCT_NAME =~ Framework ]] || [[ $PRODUCT_NAME =~ "Laptop 13" ]] || [[ $PRODUCT_NAME =~ "Laptop 16" ]] || [[ $PRODUCT_NAME =~ "Laptop 12" ]] || [[ $PRODUCT_NAME =~ "Desktop" ]]; then + check_framework_distro_compatibility + fi + + echo "" >> "$output_file" + + # Add detailed hardware context + get_device_details +} + +# Check if current distro/version is recommended for detected Framework hardware +check_framework_distro_compatibility() { + local current_distro=$(grep ^ID= /etc/os-release | awk -F= '{print $2}' | tr -d '"') + local current_version=$(grep ^VERSION_ID= /etc/os-release | awk -F= '{print $2}' | tr -d '"') + local framework_model="$FRAMEWORK_MODEL" + local product_name="$PRODUCT_NAME" + + # Framework support levels based on EXACT frame.work/linux page content + local support_level="" + local recommendation_msg="" + local model_name="" + + # Framework Laptop 12 (13th Gen Intel® Core™) + if [[ $framework_model =~ "Framework Laptop 12" ]] || [[ $product_name =~ "Laptop 12" ]]; then + model_name="Framework Laptop 12" + case "$current_distro" in + fedora) + if [[ $current_version == "42" ]]; then + support_level="OFFICIALLY_SUPPORTED" + else + support_level="UNTESTED" + recommendation_msg="Framework Laptop 12 only supports Fedora 42. Current: Fedora $current_version is not listed as supported" + fi + ;; + ubuntu) + if [[ $current_version == "25.04" ]]; then + support_level="OFFICIALLY_SUPPORTED" + else + support_level="UNTESTED" + recommendation_msg="Framework Laptop 12 only supports Ubuntu 25.04. Current: Ubuntu $current_version is not listed as supported" + fi + ;; + bazzite) + support_level="OFFICIALLY_SUPPORTED" + ;; + bluefin|arch|linuxmint) + support_level="COMPATIBLE_COMMUNITY_SUPPORTED" + ;; + nixos) + if [[ $current_version == "25.05" ]]; then + support_level="COMPATIBLE_COMMUNITY_SUPPORTED" + else + support_level="UNTESTED" + recommendation_msg="Framework Laptop 12 only supports NixOS 25.05. Current: NixOS $current_version is not listed as supported" + fi + ;; + *) + support_level="UNTESTED" + recommendation_msg="Framework Laptop 12 officially supports: Fedora 42, Ubuntu 25.04, Bazzite. Community supported: Project Bluefin, Arch Linux, Linux Mint, NixOS 25.05. Your current distribution may not be fully compatible" + ;; + esac + + # Framework Desktop (AMD Ryzen™ AI Max 300 Series) + elif [[ $framework_model =~ "Framework Desktop" ]] || [[ $product_name =~ "Desktop" ]]; then + model_name="Framework Desktop" + case "$current_distro" in + fedora) + if [[ $current_version == "42" ]]; then + support_level="OFFICIALLY_SUPPORTED" + else + support_level="UNTESTED" + recommendation_msg="Framework Desktop only supports Fedora 42. Current: Fedora $current_version is not listed as supported" + fi + ;; + bazzite) + support_level="OFFICIALLY_SUPPORTED" + ;; + arch|bluefin) + support_level="COMPATIBLE_COMMUNITY_SUPPORTED" + ;; + nixos) + if [[ $current_version == "25.05" ]]; then + support_level="COMPATIBLE_COMMUNITY_SUPPORTED" + else + support_level="UNTESTED" + recommendation_msg="Framework Desktop only supports NixOS 25.05. Current: NixOS $current_version is not listed as supported" + fi + ;; + *) + support_level="UNTESTED" + recommendation_msg="Framework Desktop officially supports: Fedora 42, Bazzite. Community supported: Arch Linux, NixOS 25.05, Project Bluefin. Your current distribution may not be fully compatible" + ;; + esac + + # Framework Laptop 13 (AMD Ryzen™ AI 300 Series) + elif [[ $framework_model =~ "Framework Laptop 13" ]] && [[ $framework_model =~ "AMD Ryzen.*AI.*300" ]] || + [[ $product_name =~ "Laptop 13" ]] && [[ $product_name =~ "AMD Ryzen.*AI.*300" ]]; then + model_name="Framework Laptop 13 (AMD Ryzen AI 300)" + case "$current_distro" in + fedora) + if [[ $current_version == "42" ]]; then + support_level="OFFICIALLY_SUPPORTED" + else + support_level="UNTESTED" + recommendation_msg="Framework Laptop 13 (AMD Ryzen AI 300) only supports Fedora 42. Current: Fedora $current_version is not listed as supported" + fi + ;; + bazzite) + support_level="OFFICIALLY_SUPPORTED" + ;; + arch|bluefin) + support_level="COMPATIBLE_COMMUNITY_SUPPORTED" + ;; + nixos) + if [[ $current_version == "25.05" ]]; then + support_level="COMPATIBLE_COMMUNITY_SUPPORTED" + else + support_level="UNTESTED" + recommendation_msg="Framework Laptop 13 (AMD Ryzen AI 300) only supports NixOS 25.05. Current: NixOS $current_version is not listed as supported" + fi + ;; + *) + support_level="UNTESTED" + recommendation_msg="Framework Laptop 13 (AMD Ryzen AI 300) officially supports: Fedora 42, Bazzite. Community supported: Arch Linux, NixOS 25.05, Project Bluefin. Your current distribution may not be fully compatible" + ;; + esac + + # Framework Laptop 13 (Intel® Core™ Ultra Series 1) + elif [[ $framework_model =~ "Framework Laptop 13" ]] && [[ $framework_model =~ "Intel.*Core.*Ultra" ]] || + [[ $product_name =~ "Laptop 13" ]] && [[ $product_name =~ "Intel.*Core.*Ultra" ]]; then + model_name="Framework Laptop 13 (Intel Core Ultra)" + case "$current_distro" in + fedora) + if [[ $current_version == "41" || $current_version == "42" ]]; then + support_level="OFFICIALLY_SUPPORTED" + else + support_level="UNTESTED" + recommendation_msg="Framework Laptop 13 (Intel Core Ultra) only supports Fedora 41/42. Current: Fedora $current_version is not listed as supported" + fi + ;; + ubuntu) + # Ubuntu 24.04+ means 24.04 and later + if [[ $current_version == "24.04" || $current_version > "24.04" ]]; then + support_level="OFFICIALLY_SUPPORTED" + else + support_level="UNTESTED" + recommendation_msg="Framework Laptop 13 (Intel Core Ultra) requires Ubuntu 24.04 or newer. Current: Ubuntu $current_version is not supported" + fi + ;; + bazzite) + support_level="OFFICIALLY_SUPPORTED" + ;; + bluefin|arch|linuxmint) + support_level="COMPATIBLE_COMMUNITY_SUPPORTED" + ;; + nixos) + if [[ $current_version == "25.05" ]]; then + support_level="COMPATIBLE_COMMUNITY_SUPPORTED" + else + support_level="UNTESTED" + recommendation_msg="Framework Laptop 13 (Intel Core Ultra) only supports NixOS 25.05. Current: NixOS $current_version is not listed as supported" + fi + ;; + *) + support_level="UNTESTED" + recommendation_msg="Framework Laptop 13 (Intel Core Ultra) officially supports: Fedora 41/42, Ubuntu 24.04+, Bazzite. Community supported: Project Bluefin, Arch Linux, Linux Mint, NixOS 25.05. Your current distribution may not be fully compatible" + ;; + esac + + # Framework Laptop 16 (AMD Ryzen™ 7040 Series) + elif [[ $framework_model =~ "Framework Laptop 16" ]] || [[ $product_name =~ "Laptop 16" ]]; then + model_name="Framework Laptop 16" + case "$current_distro" in + fedora) + if [[ $current_version == "42" ]]; then + support_level="OFFICIALLY_SUPPORTED" + else + support_level="UNTESTED" + recommendation_msg="Framework Laptop 16 only supports Fedora 42. Current: Fedora $current_version is not listed as supported" + fi + ;; + ubuntu) + if [[ $current_version == "24.04" || $current_version > "24.04" || $current_version == "22.04" ]]; then + support_level="OFFICIALLY_SUPPORTED" + else + support_level="UNTESTED" + recommendation_msg="Framework Laptop 16 supports Ubuntu 24.04+, 22.04 LTS. Current: Ubuntu $current_version is not listed as supported" + fi + ;; + bazzite) + support_level="OFFICIALLY_SUPPORTED" + ;; + bluefin|arch|linuxmint) + support_level="COMPATIBLE_COMMUNITY_SUPPORTED" + ;; + nixos) + if [[ $current_version == "24.11" || $current_version > "24.11" ]]; then + support_level="COMPATIBLE_COMMUNITY_SUPPORTED" + else + support_level="UNTESTED" + recommendation_msg="Framework Laptop 16 requires NixOS 24.11 or newer. Current: NixOS $current_version is not supported" + fi + ;; + *) + support_level="UNTESTED" + recommendation_msg="Framework Laptop 16 officially supports: Fedora 42, Ubuntu 24.04+/22.04 LTS, Bazzite. Community supported: Project Bluefin, Arch Linux, NixOS 24.11+, Linux Mint. Your current distribution may not be fully compatible" + ;; + esac + + # Framework Laptop 13 (older generations) - fallback for other Intel/AMD variants + elif [[ $framework_model =~ "Framework Laptop 13" ]] || [[ $product_name =~ "Laptop 13" ]]; then + model_name="Framework Laptop 13" + case "$current_distro" in + fedora) + if [[ $current_version == "42" ]]; then + support_level="OFFICIALLY_SUPPORTED" + else + support_level="UNTESTED" + recommendation_msg="Framework Laptop 13 only supports Fedora 42. Current: Fedora $current_version is not listed as supported" + fi + ;; + ubuntu) + if [[ $current_version == "24.04" || $current_version > "24.04" || $current_version == "22.04" ]]; then + support_level="OFFICIALLY_SUPPORTED" + else + support_level="UNTESTED" + recommendation_msg="Framework Laptop 13 supports Ubuntu 24.04+, 22.04 LTS. Current: Ubuntu $current_version is not listed as supported" + fi + ;; + bazzite) + support_level="OFFICIALLY_SUPPORTED" + ;; + manjaro|linuxmint|arch) + support_level="COMPATIBLE_COMMUNITY_SUPPORTED" + ;; + nixos) + if [[ $current_version == "24.11" || $current_version > "24.11" ]]; then + support_level="COMPATIBLE_COMMUNITY_SUPPORTED" + else + support_level="UNTESTED" + recommendation_msg="Framework Laptop 13 requires NixOS 24.11 or newer. Current: NixOS $current_version is not supported" + fi + ;; + *) + support_level="UNTESTED" + recommendation_msg="Framework Laptop 13 officially supports: Fedora 42, Ubuntu 24.04+/22.04 LTS, Bazzite. Community supported: Manjaro XFCE, Linux Mint, Arch Linux, NixOS 24.11+. Your current distribution may not be fully compatible" + ;; + esac + fi + + # Generate appropriate recommendations based on support level with plain English + if [[ -n $model_name ]]; then + case "$support_level" in + "OFFICIALLY_SUPPORTED") + echo "INFORMATIONAL|DISTRO_COMPATIBILITY|✅ Your Linux distribution ($current_distro $current_version) is officially supported and tested by Framework for your $model_name → You should have the best experience and full hardware support" >> "$recommendations_file" + ;; + "COMPATIBLE_COMMUNITY_SUPPORTED") + echo "INFORMATIONAL|DISTRO_COMPATIBILITY|🔵 Your Linux distribution ($current_distro $current_version) is community supported for your $model_name → Most features should work well, but you may need to install additional drivers or make minor tweaks" >> "$recommendations_file" + ;; + "UNTESTED") + echo "INFORMATIONAL|DISTRO_COMPATIBILITY|⚠️ Your Linux distribution may not be fully compatible with your $model_name → $recommendation_msg For the best experience, consider switching to a supported distribution from frame.work/linux" >> "$recommendations_file" + ;; + esac + fi +} + +# Function to process logs - FIXED to avoid subshell issues +process_logs() { + local start_time=$1 + local end_time=$2 + + local start_seconds=$(date -d "$start_time" +%s) + local end_seconds=$(date -d "$end_time" +%s) + + # Create a header for dmesg section with spacing + echo "===== dmesg output starts =====" >> "$output_file" + echo "" >> "$output_file" + + # Process dmesg - FIXED: Use process substitution instead of pipe to avoid subshell + local total_lines=$(sudo dmesg | wc -l) + local current_line=0 + + while IFS= read -r line; do + ((current_line++)) + local percentage=$((current_line * 100 / total_lines)) + show_progress_with_context $percentage "dmesg logs" 2>/dev/null + + if [[ $line =~ \[(.*?)\] ]]; then + local timestamp="${BASH_REMATCH[1]}" + if date -d "$timestamp" &>/dev/null; then + local line_seconds=$(date -d "$timestamp" +%s) + if (( line_seconds >= start_seconds && line_seconds <= end_seconds )); then + echo "$line" >> "$output_file" + if [[ $line =~ error|warning|fail|critical|failed|timeout|crash|disconnected|deauth ]]; then + add_to_summary "$line" "$timestamp" + fi + fi + fi + fi + done < <(sudo dmesg -T) 2>/dev/null + + echo -e "\n${GREEN}✅ Dmesg analysis complete${RESET}" + echo "" >> "$output_file" + + # Create a header for journalctl section with spacing + echo "===== journalctl output starts =====" >> "$output_file" + echo "" >> "$output_file" + + # Process journalctl - FIXED: Use process substitution instead of pipe to avoid subshell + total_lines=$(sudo journalctl --since="$start_time" --until="$end_time" 2>/dev/null | wc -l) + current_line=0 + + while IFS= read -r line; do + ((current_line++)) + percentage=$((current_line * 100 / total_lines)) + show_progress_with_context $percentage "journal logs" + echo "$line" >> "$output_file" + + # Extract timestamp from journalctl line + local journal_timestamp="" + if [[ $line =~ ^([A-Z][a-z]{2}\ [0-9]{2}\ [0-9]{2}:[0-9]{2}:[0-9]{2}) ]]; then + journal_timestamp="${BASH_REMATCH[1]}" + fi + + if [[ $line =~ error|warning|fail|critical|failed|timeout|crash|disconnected|deauth ]]; then + add_to_summary "$line" "$journal_timestamp" + fi + done < <(sudo journalctl --since="$start_time" --until="$end_time" 2>/dev/null) + + echo -e "${GREEN}✅ Journal analysis complete${RESET}" +} + +# Function to generate intelligent recommendations with plain English +generate_recommendations() { + local file=$1 + + # Add intelligent recommendations section + echo "" >> "$file" + echo "===== INTELLIGENT RECOMMENDATIONS =====" >> "$file" + echo "" >> "$file" + + # Check if recommendations file exists and has content + if [ -f "$recommendations_file" ] && [ -s "$recommendations_file" ]; then + # Sort and process recommendations by severity + declare -A severity_colors + severity_colors[IMMEDIATE]="🔴 IMMEDIATE" + severity_colors[URGENT]="🟠 URGENT" + severity_colors[IMPORTANT]="🟡 IMPORTANT" + severity_colors[INFORMATIONAL]="🔵 INFORMATIONAL" + severity_colors[PREVENTIVE]="🟢 PREVENTIVE" + + # Process recommendations by severity order + for severity in IMMEDIATE URGENT IMPORTANT INFORMATIONAL PREVENTIVE; do + local recommendations=$(grep "^$severity|" "$recommendations_file" 2>/dev/null | sort | uniq) + if [ -n "$recommendations" ]; then + # Check if INFORMATIONAL contains positive confirmations vs warnings + if [[ $severity == "INFORMATIONAL" ]]; then + local has_warnings=$(echo "$recommendations" | grep -v "✅" | wc -l) + if [[ $has_warnings -gt 0 ]]; then + echo "${severity_colors[$severity]} Actions Required:" >> "$file" + else + echo "${severity_colors[$severity]} Status:" >> "$file" + fi + else + echo "${severity_colors[$severity]} Actions Required:" >> "$file" + fi + echo "" >> "$file" + + echo "$recommendations" | while IFS='|' read -r sev category rec; do + echo "• [$category] $rec" >> "$file" + done + echo "" >> "$file" + fi + done + + # Pattern analysis for multiple event types with plain English explanations + pattern_analysis_added=false + + # WiFi drops - transparent reporting +wifi_drops=$(grep -c "WIFI_DROP" "$state_changes_file" 2>/dev/null | head -1) +wifi_drops=${wifi_drops:-0} +if [ "$wifi_drops" -gt 3 ]; then + if [ "$pattern_analysis_added" = false ]; then + echo "🟡 IMPORTANT Actions Required (Pattern Analysis):" >> "$file" + echo "" >> "$file" + pattern_analysis_added=true + fi + echo "• [WIFI_ACTIVITY] Detected $wifi_drops WiFi disconnection events → This could be normal power management or actual WiFi problems. Run the Enhanced WiFi Analyzer tool to determine if there are real issues: https://github.com/FrameworkComputer/linux-docs/tree/main/Enhanced-WiFi-Analyzer" >> "$file" +fi + + # USB reconnection patterns + usb_reconnects=$(grep -c "USB_RECONNECT" "$state_changes_file" 2>/dev/null | head -1) + usb_reconnects=${usb_reconnects:-0} + if [ "$usb_reconnects" -gt 2 ]; then + if [ "$pattern_analysis_added" = false ]; then + echo "🟡 IMPORTANT Actions Required (Pattern Analysis):" >> "$file" + echo "" >> "$file" + pattern_analysis_added=true + fi + echo "• [USB_INSTABILITY] Your USB devices have reconnected $usb_reconnects times → This means USB ports or expansion cards may be loose. Unplug and firmly reconnect all USB devices and expansion cards" >> "$file" + fi + + # GPU hang/recovery patterns + gpu_hangs=$(grep -c "GPU_HANG" "$state_changes_file" 2>/dev/null | head -1) + gpu_hangs=${gpu_hangs:-0} + if [ "$gpu_hangs" -gt 1 ]; then + if [ "$pattern_analysis_added" = false ]; then + echo "🟡 IMPORTANT Actions Required (Pattern Analysis):" >> "$file" + echo "" >> "$file" + pattern_analysis_added=true + fi + echo "• [GPU_INSTABILITY] Your graphics card has crashed $gpu_hangs times → This suggests serious graphics problems. Check that you're using the correct charger, update graphics drivers, and contact Framework support if this continues" >> "$file" + fi + + # Thermal throttling patterns + thermal_events=$(grep -c "THERMAL_THROTTLE" "$state_changes_file" 2>/dev/null | head -1) + thermal_events=${thermal_events:-0} + if [ "$thermal_events" -gt 2 ]; then + if [ "$pattern_analysis_added" = false ]; then + echo "🟡 IMPORTANT Actions Required (Pattern Analysis):" >> "$file" + echo "" >> "$file" + pattern_analysis_added=true + fi + echo "• [THERMAL_CYCLING] Your laptop has overheated $thermal_events times → This means the cooling system is struggling. Clean the air vents, close demanding programs, and consider using your laptop on a hard surface for better airflow" >> "$file" + fi + + if [ "$pattern_analysis_added" = true ]; then + echo "" >> "$file" + fi + + else + echo "✅ No issues detected requiring immediate action." >> "$file" + fi + + echo "" >> "$file" +} + +# Function to add summaries to the file +add_summaries() { + local file=$1 + + # Add critical error summary section first + echo "===== Critical Error Summary =====" >> "$file" + echo "Actual system errors requiring attention:" >> "$file" + echo "" >> "$file" + + if [ -s "$focused_summary_file" ]; then + sort "$focused_summary_file" | uniq -c | sort -rn >> "$file" + else + echo "✅ No critical system errors detected in the logs." >> "$file" + fi + + echo "" >> "$file" + + # Add general summary section + echo "===== All Error/Warning Messages (excluding noise) =====" >> "$file" + echo "" >> "$file" + + if [ -s "$summary_file" ]; then + sort "$summary_file" | uniq -c | sort -rn >> "$file" + else + echo "No error or warning messages found in the logs (excluding gnome-shell and benign Framework messages)." >> "$file" + fi + + echo "" >> "$file" + + # Add diagnostic completion summary + echo "===== DIAGNOSTIC COMPLETION SUMMARY =====" >> "$file" + echo "Scan completed: $(date)" >> "$file" + local total_issues=$(wc -l < "$summary_file" 2>/dev/null || echo "0") + local critical_issues=$(wc -l < "$focused_summary_file" 2>/dev/null || echo "0") + local recommendations_count=$(wc -l < "$recommendations_file" 2>/dev/null || echo "0") + echo "Total issues found: $total_issues" >> "$file" + echo "Potentially important issues: $critical_issues" >> "$file" + echo "Recommendations generated: $recommendations_count" >> "$file" +} + +######################################## +# Main script starts here +######################################## + +echo -e "${BOLD}${CYAN}Framework Laptop Enhanced Diagnostic Tool${RESET}" +echo -e "${BOLD}==========================================${RESET}" +echo "" +echo "Choose an option:" +echo "1. Last x minutes" +echo "2. Last 24 hours" +echo "3. Specific time range" +echo "4. Filter previously created log file" +read choice + +case $choice in + 1) + echo "Enter the number of minutes:" + read minutes + start_time=$(date -d "$minutes minutes ago" '+%Y-%m-%d %H:%M') + end_time=$(date '+%Y-%m-%d %H:%M') + get_system_info 2>/dev/null + process_logs "$start_time" "$end_time" + + # Insert recommendations RIGHT AFTER system info by rebuilding the file + temp_file="/tmp/temp_rebuild_temp_$$_$(date +%s).txt" + + # Extract everything up to and including the entire Hardware Context section + sed -n '1,/^Hardware Context:/p' "$output_file" > "$temp_file" + sed -n '/^ GPU:/,/^$/p' "$output_file" >> "$temp_file" + + # Add recommendations immediately after hardware context + generate_recommendations "$temp_file" + + # Add the rest (dmesg and journalctl) + sed -n '/^===== dmesg output starts =====/,$p' "$output_file" >> "$temp_file" + + # Replace original with rebuilt version + mv "$temp_file" "$output_file" + + add_summaries "$output_file" + ;; + 2) + start_time=$(date -d "24 hours ago" '+%Y-%m-%d %H:%M') + end_time=$(date '+%Y-%m-%d %H:%M') + get_system_info + process_logs "$start_time" "$end_time" + + # Insert recommendations RIGHT AFTER system info by rebuilding the file + temp_file="/tmp/temp_rebuild_temp_$$_$(date +%s).txt" + + # Extract everything up to and including the entire Hardware Context section + sed -n '1,/^Hardware Context:/p' "$output_file" > "$temp_file" + sed -n '/^ GPU:/,/^$/p' "$output_file" >> "$temp_file" + + # Add recommendations immediately after hardware context + generate_recommendations "$temp_file" + + # Add the rest (dmesg and journalctl) + sed -n '/^===== dmesg output starts =====/,$p' "$output_file" >> "$temp_file" + + # Replace original with rebuilt version + mv "$temp_file" "$output_file" + + add_summaries "$output_file" + ;; + 3) + echo "Enter the start time (YYYY-MM-DD HH:MM):" + read start_time + echo "Enter the end time (YYYY-MM-DD HH:MM):" + read end_time + get_system_info + process_logs "$start_time" "$end_time" + + # Insert recommendations RIGHT AFTER system info by rebuilding the file + temp_file="/tmp/temp_rebuild_temp_$$_$(date +%s).txt" + + # Extract everything up to and including the entire Hardware Context section + sed -n '1,/^Hardware Context:/p' "$output_file" > "$temp_file" + sed -n '/^ GPU:/,/^$/p' "$output_file" >> "$temp_file" + + # Add recommendations immediately after hardware context + generate_recommendations "$temp_file" + + # Add the rest (dmesg and journalctl) + sed -n '/^===== dmesg output starts =====/,$p' "$output_file" >> "$temp_file" + + # Replace original with rebuilt version + mv "$temp_file" "$output_file" + + add_summaries "$output_file" + ;; + 4) + echo "Looking for file called combined_log.txt in current directory..." + if [ ! -f "$output_file" ]; then + echo -e "${RED}File not found: $output_file${RESET}" + exit 1 + fi + echo -e "${GREEN}File found. Proceeding with filtering options.${RESET}" + + echo "Choose filtering option:" + echo "1. Grep for a key phrase" + echo "2. Grep for a keyword" + read grep_choice + + case $grep_choice in + 1) + echo "Enter the key phrase to grep for:" + read key_phrase + key_phrase=$(echo "$key_phrase" | xargs) # Trim whitespace + echo "Searching for: '$key_phrase'" + grep -i "$key_phrase" "$output_file" > "$filtered_output_file" + ;; + 2) + echo "Enter the keyword to grep for:" + read keyword + keyword=$(echo "$keyword" | xargs) # Trim whitespace + echo "Searching for: '$keyword'" + grep -i "$keyword" "$output_file" > "$filtered_output_file" + ;; + *) + echo -e "${RED}Invalid choice. No filtering applied.${RESET}" + exit 1 + ;; + esac + + if [ ! -s "$filtered_output_file" ]; then + echo -e "${YELLOW}No matches found. Filtered log file is empty.${RESET}" + exit 1 + fi + + echo -e "\n${BOLD}${GREEN}Filtered log saved in $filtered_output_file${RESET}" + line_count=$(wc -l < "$filtered_output_file") + echo -e "${BOLD}Total lines in filtered output: $line_count${RESET}" + ;; + *) + echo -e "${RED}Invalid choice${RESET}" + exit 1 + ;; +esac + +# Cleanup happens automatically via trap +# Final output message - only show for diagnostic runs, not filtering +if [ "$choice" != "4" ]; then + echo "" + echo -e "${BOLD}${GREEN}✅ Diagnostic complete!${RESET}" + echo -e "${BOLD}📋 Full report saved to: $output_file${RESET}" + echo -e "${BOLD}🔍 Check the 'INTELLIGENT RECOMMENDATIONS' section for actionable solutions${RESET}" + + # Display summary of findings + if [ -f "$output_file" ]; then + echo "" + echo -e "${BOLD}${CYAN}Quick Summary:${RESET}" + + # Check if we detected a Framework device + if grep -q "Framework device detected" "$output_file" 2>/dev/null; then + detected_model=$(grep "Detected:" "$output_file" 2>/dev/null | head -1 | awk '{print substr($0, index($0,$2))}' | xargs) + if [ -n "$detected_model" ]; then + echo -e "${GREEN}🖥️ $detected_model${RESET}" + fi + fi + + # Show current temperature if available + temp_reading=$(grep "Current CPU:" "$output_file" 2>/dev/null | head -1) + if [ -n "$temp_reading" ]; then + echo -e "${BLUE}🌡️ $temp_reading${RESET}" + fi + + # Show critical issues count - count everything EXCEPT INFORMATIONAL + # Handle case where no recommendations section exists + if grep -q "INTELLIGENT RECOMMENDATIONS" "$output_file" 2>/dev/null; then + total_bullets=$(grep -A 1000 "INTELLIGENT RECOMMENDATIONS" "$output_file" 2>/dev/null | grep -c "^• \[") + informational_bullets=$(grep -A 20 "🔵 INFORMATIONAL" "$output_file" 2>/dev/null | grep -c "^• \[") + else + total_bullets=0 + informational_bullets=0 + fi + + # Ensure we have valid numbers + total_bullets=${total_bullets:-0} + informational_bullets=${informational_bullets:-0} + + # Total issues = all bullets minus informational bullets + critical_count=$((total_bullets - informational_bullets)) + + # Ensure variables are numeric + critical_count=${critical_count:-0} + + if [ "$critical_count" -gt 0 ]; then + echo -e "${RED}⚠️ $critical_count issues found${RESET}" + else + echo -e "${GREEN}✅ No issues detected${RESET}" + fi + + # Show if running supported distro + if grep -q "officially supported and tested" "$output_file" 2>/dev/null; then + echo -e "${GREEN}✅ Running officially supported Linux distribution${RESET}" + elif grep -q "community supported" "$output_file" 2>/dev/null; then + echo -e "${BLUE}🔵 Running community supported Linux distribution${RESET}" + fi + + echo "" + echo -e "${BOLD}For detailed analysis, open: $output_file${RESET}" + + # Provide quick access commands + echo "" + echo -e "${BOLD}${CYAN}Quick Commands:${RESET}" + echo -e "${YELLOW}View full report:${RESET} cat \"$output_file\"" + echo -e "${YELLOW}View recommendations only:${RESET} grep -A 20 \"INTELLIGENT RECOMMENDATIONS\" \"$output_file\"" + echo -e "${YELLOW}View current temps:${RESET} sensors" + echo -e "${YELLOW}Monitor temps:${RESET} watch -n 2 sensors" + + # Framework-specific quick links + if grep -q "Framework device detected" "$output_file" 2>/dev/null; then + echo "" + echo -e "${BOLD}${CYAN}Framework Resources:${RESET}" + echo -e "${GREEN}Support:${RESET} https://frame.work/support" + echo -e "${GREEN}Linux Guides:${RESET} https://frame.work/linux" + echo -e "${GREEN}Community forum:${RESET} https://community.frame.work/" + echo -e "${GREEN}Linux docs:${RESET} https://github.com/FrameworkComputer/linux-docs" + echo -e "${GREEN}Linux KnowledgeBase Articles:${RESET} https://knowledgebase.frame.work/categories/linux-S1IUEcFbkx" + echo -e "${GREEN}Linux Tools and Scripts:${RESET} https://knowledgebase.frame.work/linux-on-framework-tools-and-scripts-rymax1Jdyg" + + # Show WiFi analyzer if WiFi issues detected + if grep -q "WiFi has disconnected\|WiFi firmware crashed" "$output_file" 2>/dev/null; then + echo -e "${YELLOW}WiFi issues detected - Enhanced WiFi Analyzer:${RESET}" + echo "https://github.com/FrameworkComputer/linux-docs/tree/main/Enhanced-WiFi-Analyzer" + fi + fi + fi +fi + +# Exit with appropriate code based on findings +if [ "$choice" != "4" ] && [ -f "$output_file" ]; then + # Check for critical issues - count everything EXCEPT INFORMATIONAL + if grep -q "INTELLIGENT RECOMMENDATIONS" "$output_file" 2>/dev/null; then + total_bullets=$(grep -A 1000 "INTELLIGENT RECOMMENDATIONS" "$output_file" 2>/dev/null | grep -c "^• \[") + informational_bullets=$(grep -A 20 "🔵 INFORMATIONAL" "$output_file" 2>/dev/null | grep -c "^• \[") + else + total_bullets=0 + informational_bullets=0 + fi + + # Ensure we have valid numbers + total_bullets=${total_bullets:-0} + informational_bullets=${informational_bullets:-0} + + critical_issues=$((total_bullets - informational_bullets)) + critical_issues=${critical_issues:-0} + if [ "$critical_issues" -gt 0 ]; then + exit 1 # Exit with error code if critical issues found + fi +fi + +exit 0 diff --git a/log-helper/how-it-works.md b/log-helper/how-it-works.md new file mode 100644 index 0000000..68de2a9 --- /dev/null +++ b/log-helper/how-it-works.md @@ -0,0 +1,129 @@ +## How it works + +**[BACK TO MAIN PAGE](https://github.com/FrameworkComputer/linux-docs/tree/main/log-helper#framework-log-helper-aka-combinedsh)** + +### Main Features: +- **Intelligent System Analysis**: Automatically detects Framework laptop models and applies model-specific diagnostics +- **Advanced Log Processing**: Gathers logs from dmesg (kernel messages) and journalctl (system logs) with intelligent noise filtering +- **Real-time Hardware Monitoring**: Checks current temperatures, network connectivity, power status, and battery health +- **Plain English Recommendations**: Translates technical errors into understandable explanations with actionable solutions +- **Framework-Specific Features**: Validates Linux distribution compatibility, applies appropriate thermal thresholds, and provides model-specific recommendations +- **Pattern Analysis**: Tracks recurring issues like WiFi drops, USB reconnections, and thermal cycling +- **Context-Aware Error Detection**: Identifies error sequences and hardware failure patterns rather than isolated events + +### How it's used: +1. **Choose from four options:** + - Collect logs from the last X minutes + - Collect logs from the last 24 hours + - Collect logs for a specific time range + - Filter a previously created log file + +2. **For new log collection (options 1-3):** + - **System Detection**: Automatically identifies your Framework model (Laptop 12/13/16, Desktop) and applies appropriate diagnostics + - **Hardware Analysis**: Gathers detailed system information including CPU, GPU, WiFi card, RAM, and storage details + - **Real-time Status Checks**: Tests internet connectivity, checks current temperatures with model-specific thresholds, monitors power/battery status + - **Distribution Compatibility**: Validates if your Linux distribution is officially supported by Framework for your specific model + - **Intelligent Log Processing**: Collects and analyzes logs while filtering out harmless system noise (GNOME UI messages, normal systemd operations) + - **Smart Error Analysis**: Identifies actual problems vs routine system operations, translates technical errors into plain English + - **Pattern Recognition**: Tracks device state changes, USB connections, thermal events, and recurring issues + - **Actionable Recommendations**: Generates severity-based recommendations (🔴 Immediate, 🟠 Urgent, 🟡 Important, 🔵 Informational, 🟢 Preventive) + +3. **For filtering an existing log file (option 4):** + - Looks for "combined_log.txt" in the current directory + - Allows searching for specific keywords or phrases within the log + - Saves filtered results to "filtered_log.txt" + +4. **Progress Indicators**: Displays visual progress bars with context-aware status messages during analysis + +5. **Comprehensive Output Structure:** + - **System Information**: Hardware detection, Framework model identification, real-time status + - **Intelligent Recommendations**: Prioritized by severity with plain English explanations + - **Critical Error Summary**: Filtered list of actual problems requiring attention + - **Complete Log Analysis**: Full dmesg and journalctl output with noise filtering + +### Key Benefits: +- **Framework-Optimized**: Specifically designed for Framework laptops with model-specific knowledge +- **User-Friendly**: Plain English explanations instead of technical jargon +- **Intelligent Filtering**: Distinguishes between actual problems and normal system operations +- **Real-time Analysis**: Shows current system status, not just historical logs +- **Actionable Intelligence**: Provides specific solutions and recommendations +- **Pattern Detection**: Identifies recurring issues and provides targeted fixes +- **Distribution Awareness**: Validates Linux compatibility and suggests supported distributions + +### Framework-Specific Intelligence: + +#### **Thermal Management** +- **AMD Ryzen AI 300 Series**: Recognizes these run hotter by design (95°C is normal) +- **AMD 7040 Series**: Appropriate thresholds for Framework Laptop 16 +- **Intel Processors**: Conservative thermal limits for Intel-based Framework laptops +- **Real-time Monitoring**: Current CPU/GPU temperatures with model-appropriate warnings + +#### **Power System Analysis** +- **Charger Verification**: Recommends correct wattage (60W for Laptop 13, 180W for Laptop 16) +- **USB-C Power Delivery**: Monitors PD negotiation issues +- **Battery Health**: Comprehensive battery condition assessment + +#### **Distribution Compatibility** +- **Official Support**: Validates against Framework's supported distributions +- **Version-Specific**: Ensures correct versions (Fedora 42, Ubuntu 24.04+, etc.) +- **Community Support**: Recognizes community-supported distributions + +### Output Sections: + +``` +===== System Information ===== +Hardware detection, Framework model identification, distribution compatibility + +===== INTELLIGENT RECOMMENDATIONS ===== +🔴 IMMEDIATE Actions Required: +🟠 URGENT Actions Required: +🟡 IMPORTANT Actions Required: +🔵 INFORMATIONAL Status: +🟢 PREVENTIVE Actions Required: + +===== dmesg output starts ===== +Kernel messages with intelligent filtering + +===== journalctl output starts ===== +System service logs with noise reduction + +===== Critical Error Summary ===== +Actual system errors requiring attention + +===== All Error/Warning Messages (excluding noise) ===== +Comprehensive issue analysis + +===== DIAGNOSTIC COMPLETION SUMMARY ===== +Scan statistics and completion details +``` + +### Plain English Error Translation Examples: +- `amdgpu ring timeout` → "Your computer's graphics stopped working and might have crashed" +- `thermal critical temperature` → "Your computer got dangerously hot and will shut down to protect itself" +- `USB device not accepting address` → "A USB device couldn't connect properly" +- `nvme I/O timeout` → "Your main storage drive is having trouble responding" + +-------------------------------------- + +## Troubleshooting + +**If you find the script is not working right or taking over 10 minutes**, you can run this to trim down your journal to make it easier to manage: + +```bash +sudo journalctl --vacuum-time=30d --vacuum-size=500M +``` +(Then reboot and run the script again) + +**Your log file keeps getting overwritten:** +> This is by design. If you wish to keep previous logs, copy your combined_log.txt file to another location before running the script again. + +**Missing required tools on NixOS:** +> The script will show you which packages to add to your configuration.nix file and rebuild your system. + +**Script shows "Package installation failed":** +> Ensure you have internet connectivity and proper sudo permissions. The script automatically installs required diagnostic tools. + +**No temperature readings shown:** +> Run `sudo sensors-detect` and answer 'yes' to all questions, then reboot and try again. + +**[BACK TO MAIN PAGE](https://github.com/FrameworkComputer/linux-docs/tree/main/log-helper#framework-log-helper-aka-combinedsh)** diff --git a/log-helper/images/1.gif b/log-helper/images/1.gif new file mode 100644 index 0000000..7649869 Binary files /dev/null and b/log-helper/images/1.gif differ diff --git a/log-helper/images/1.png b/log-helper/images/1.png new file mode 100644 index 0000000..1291fe3 Binary files /dev/null and b/log-helper/images/1.png differ diff --git a/log-helper/images/2.gif b/log-helper/images/2.gif new file mode 100644 index 0000000..f93e152 Binary files /dev/null and b/log-helper/images/2.gif differ diff --git a/log-helper/images/2.png b/log-helper/images/2.png new file mode 100644 index 0000000..f67d91f Binary files /dev/null and b/log-helper/images/2.png differ diff --git a/log-helper/images/3.gif b/log-helper/images/3.gif new file mode 100644 index 0000000..bd7d43f Binary files /dev/null and b/log-helper/images/3.gif differ diff --git a/log-helper/images/3.png b/log-helper/images/3.png new file mode 100644 index 0000000..e0288cf Binary files /dev/null and b/log-helper/images/3.png differ diff --git a/log-helper/images/4.png b/log-helper/images/4.png new file mode 100644 index 0000000..d0f89a7 Binary files /dev/null and b/log-helper/images/4.png differ diff --git a/log-helper/images/readme b/log-helper/images/readme new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/log-helper/images/readme @@ -0,0 +1 @@ + diff --git a/log-helper/older-version.txt b/log-helper/older-version.txt new file mode 100644 index 0000000..3c1c574 --- /dev/null +++ b/log-helper/older-version.txt @@ -0,0 +1,228 @@ +#!/bin/bash + +output_file="$(pwd)/combined_log.txt" # Using current directory for input file +filtered_output_file="$(pwd)/filtered_log.txt" # Using current directory for output file +summary_file="summary_temp.txt" +focused_summary_file="focused_summary_temp.txt" + +# ANSI escape codes for text formatting +BOLD='\033[1m' +RESET='\033[0m' + +# Function to display progress bar +show_progress() { + local width=50 + local percentage=$1 + local filled=$(printf "%.0f" $(echo "$percentage * $width / 100" | bc -l)) + local empty=$((width - filled)) + printf "\rProgress: [%-${width}s] %d%%" $(printf "#%.0s" $(seq 1 $filled)) $percentage +} + +# Function to add to summary, ignoring gnome-shell errors +add_to_summary() { + if ! [[ $1 =~ gnome-shell ]]; then + echo "$1" >> "$summary_file" + if [[ $1 =~ i915|amdgpu|wayland|wifi|network|failed ]]; then + echo "$1" >> "$focused_summary_file" + fi + fi +} + +# Function to get system information +get_system_info() { + echo "===== System Information =====" > "$output_file" + echo "" >> "$output_file" + echo "Kernel version: $(uname -r)" >> "$output_file" + echo "Desktop Environment: $XDG_CURRENT_DESKTOP" >> "$output_file" + echo "Distribution: $(lsb_release -d | cut -f2)" >> "$output_file" + echo "BIOS Version: $(sudo dmidecode -s bios-version)" >> "$output_file" + echo "" >> "$output_file" +} + +# Function to process logs +process_logs() { + local start_time=$1 + local end_time=$2 + + # Convert start and end times to seconds since epoch for comparison + local start_seconds=$(date -d "$start_time" +%s) + local end_seconds=$(date -d "$end_time" +%s) + + # Create a header for dmesg section with spacing + echo "===== dmesg output starts =====" >> "$output_file" + echo "" >> "$output_file" + + # Collect and filter dmesg output with progress bar + local total_lines=$(sudo dmesg | wc -l) + local current_line=0 + + sudo dmesg -T | while IFS= read -r line; do + ((current_line++)) + local percentage=$((current_line * 100 / total_lines)) + show_progress $percentage + + if [[ $line =~ \[(.*?)\] ]]; then + local timestamp="${BASH_REMATCH[1]}" + if date -d "$timestamp" &>/dev/null; then + local line_seconds=$(date -d "$timestamp" +%s) + if (( line_seconds >= start_seconds && line_seconds <= end_seconds )); then + echo "$line" >> "$output_file" + if [[ $line =~ error|warning|fail|critical|failed ]]; then + add_to_summary "$line" + fi + fi + fi + fi + done + + echo -e "\nDmesg processing complete." + + echo "" >> "$output_file" + + # Create a header for journalctl section with spacing + echo "" >> "$output_file" + echo "===== journalctl output starts =====" >> "$output_file" + echo "" >> "$output_file" + + # Append journalctl output to the file with progress bar + total_lines=$(sudo journalctl --since="$start_time" --until="$end_time" | wc -l) + current_line=0 + + sudo journalctl --since="$start_time" --until="$end_time" | while IFS= read -r line; do + ((current_line++)) + percentage=$((current_line * 100 / total_lines)) + show_progress $percentage + echo "$line" >> "$output_file" + if [[ $line =~ error|warning|fail|critical|failed ]]; then + add_to_summary "$line" + fi + done + + echo -e "\nJournalctl processing complete." +} + +# Function to add summaries to the file +add_summaries() { + local file=$1 + + # Add focused summary section to the end of the output file + echo "" >> "$file" + echo "===== Focused Summary of Potential Issues =====" >> "$file" + echo "Issues related to i915, amdgpu, wayland, wifi, network, and failed items:" >> "$file" + echo "" >> "$file" + + if [ -s "$focused_summary_file" ]; then + sort "$focused_summary_file" | uniq -c | sort -rn >> "$file" + else + echo "No critical issues found related to graphics, display, networking, or failed items." >> "$file" + fi + + echo "" >> "$file" + + # Add general summary section to the end of the output file + echo "===== General Summary of Potential Issues (excluding gnome-shell errors) =====" >> "$file" + echo "" >> "$file" + + if [ -s "$summary_file" ]; then + sort "$summary_file" | uniq -c | sort -rn >> "$file" + else + echo "No other critical issues found in the logs (excluding gnome-shell errors)." >> "$file" + fi + + echo "" >> "$file" +} + +# Main script starts here +echo "Choose an option:" +echo "1. Last x minutes" +echo "2. Last 24 hours" +echo "3. Specific time range" +echo "4. Filter previously created log file" +read choice + +case $choice in + 1) + echo "Enter the number of minutes:" + read minutes + start_time=$(date -d "$minutes minutes ago" '+%Y-%m-%d %H:%M') + end_time=$(date '+%Y-%m-%d %H:%M') + get_system_info + process_logs "$start_time" "$end_time" + add_summaries "$output_file" + ;; + 2) + start_time=$(date -d "24 hours ago" '+%Y-%m-%d %H:%M') + end_time=$(date '+%Y-%m-%d %H:%M') + get_system_info + process_logs "$start_time" "$end_time" + add_summaries "$output_file" + ;; + 3) + echo "Enter the start time (YYYY-MM-DD HH:MM):" + read start_time + echo "Enter the end time (YYYY-MM-DD HH:MM):" + read end_time + get_system_info + process_logs "$start_time" "$end_time" + add_summaries "$output_file" + ;; + 4) + echo "Looking for file called combined_log.txt in home directory..." + if [ ! -f "$output_file" ]; then + echo "File not found: $output_file" + exit 1 + fi + echo "File found. Proceeding with filtering options." + ;; + *) + echo "Invalid choice" + exit 1 + ;; +esac + +if [ "$choice" == "4" ]; then + echo "Looking for file called combined_log.txt in current directory..." + output_file="$(pwd)/combined_log.txt" # Change to current directory + if [ ! -f "$output_file" ]; then + echo "File not found: $output_file" + exit 1 + fi + echo "File found. Proceeding with filtering options." + + echo "Choose filtering option:" + echo "1. Grep for a key phrase" + echo "2. Grep for a keyword" + read grep_choice + + case $grep_choice in + 1) + echo "Enter the key phrase to grep for:" + read key_phrase + key_phrase=$(echo "$key_phrase" | xargs) # Trim whitespace + grep -F -i -B 3 -A 5 "$key_phrase" "$output_file" > "$filtered_output_file" + ;; + 2) + echo "Enter the keyword to grep for:" + read keyword + keyword=$(echo "$keyword" | xargs) # Trim whitespace + grep -w -i -B 3 -A 5 "$keyword" "$output_file" > "$filtered_output_file" + ;; + *) + echo "Invalid choice. No filtering applied." + exit 1 + ;; + esac + + if [ ! -s "$filtered_output_file" ]; then + echo "No matches found. Filtered log file is empty." + exit 1 + fi + + echo -e "\n${BOLD}Filtered log saved in $filtered_output_file${RESET}" + line_count=$(wc -l < "$filtered_output_file") + echo -e "${BOLD}Total lines in filtered output: $line_count${RESET}" +fi + +# Remove temporary files +[ -f "$summary_file" ] && rm "$summary_file" +[ -f "$focused_summary_file" ] && rm "$focused_summary_file" diff --git a/log-helper/readme.md b/log-helper/readme.md new file mode 100644 index 0000000..fbb0058 --- /dev/null +++ b/log-helper/readme.md @@ -0,0 +1,293 @@ +### This has been retired in favor of [this new tool](https://github.com/FrameworkComputer/linux-docs/tree/main/fw-log-tool). + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +## Framework Enhanced Diagnostic Tool aka "combined.sh" + +This intelligent diagnostic script collects and analyzes system logs from your Framework laptop, automatically detecting hardware issues and providing actionable recommendations in plain English. It's specifically designed for Framework laptops with model-aware analysis and Framework-specific troubleshooting guidance. + +## Table of Contents + +- [TL;DR - Quick Start](#tldr---quick-start) +- [Key Features](#key-features) +- [Which distros does this work on?](#which-distros-does-this-work-on) +- [How to use this tool?](#how-to-use-this-tool) + - [Prerequisites](#prerequisites) + - [Quick Start](#quick-start) +- [Diagnostic Options](#diagnostic-options) + - [1. Last X Minutes](#1-last-x-minutes-) + - [2. Last 24 Hours](#2-last-24-hours-) + - [3. Specific Time Range](#3-specific-time-range-) + - [4. Filter Previously Created Log File](#4-filter-previously-created-log-file-) +- [Understanding the Results](#understanding-the-results) + - [System Information](#-system-information) + - [Intelligent Recommendations](#-intelligent-recommendations) + - [Pattern Analysis](#-pattern-analysis) + - [Framework-Specific Features](#-framework-specific-features) +- [Example Output](#example-output) +- [When to Contact Support](#when-to-contact-support) +- [Quick Commands](#quick-commands) +- [Framework Resources](#framework-resources) +- [Advanced Information](#advanced-information) + +### TL;DR - Quick Start + +**Download and run immediately:** +```bash +curl -s https://raw.githubusercontent.com/FrameworkComputer/linux-docs/main/log-helper/combined.sh -o combined.sh && chmod +x combined.sh && bash combined.sh +``` + +Choose option 1 or 2, check the "INTELLIGENT RECOMMENDATIONS" section in the output for actionable solutions. + +--- + +### Key Features + +- **🔍 Intelligent Issue Detection**: Automatically identifies GPU hangs, thermal problems, WiFi issues, USB connection problems, and more +- **🖥️ Framework-Specific Analysis**: Detects your exact Framework model and provides model-specific recommendations +- **📋 Plain English Recommendations**: Translates technical errors into clear, actionable advice +- **🌡️ Real-Time Hardware Monitoring**: Shows current temperatures, power status, and connectivity +- **⚡ Smart Noise Filtering**: Focuses on actual problems, ignoring routine system operations +- **🔧 Comprehensive Hardware Detection**: Identifies your GPU, WiFi card, storage, RAM, and expansion cards + +### Which distros does this work on? + +**This tool works with all major Linux distributions and automatically installs required packages:** + +- **Ubuntu/Debian/Linux Mint** (officially supported by Framework) +- **Fedora** (officially supported by Framework) +- **Bazzite/Project Bluefin** (officially supported by Framework) +- **Arch Linux/Manjaro/EndeavourOS** (community supported) +- **openSUSE Tumbleweed/Leap** (community supported) +- **NixOS** (community supported) +- **Pop!_OS** (community supported) + +### How to use this tool? + +#### Prerequisites + +Most systems already have curl installed. If needed: + +**Fedora:** +```bash +sudo dnf install curl -y +``` + +**Ubuntu/Debian:** +```bash +sudo apt install curl -y +``` + +**Bazzite/Bluefin:** Already included, no installation needed. + +#### Quick Start + +**Download and run the diagnostic tool:** +```bash +curl -s https://raw.githubusercontent.com/FrameworkComputer/linux-docs/main/log-helper/combined.sh -o combined.sh && chmod +x combined.sh && bash combined.sh +``` + +**For future runs:** +```bash +./combined.sh +``` + +### Diagnostic Options + +#### 1. Last X Minutes ⏰ +**Best for recent issues** + +- Select option `1` +- Enter the number of minutes (e.g., `30` for issues that happened 30 minutes ago) +- The tool will analyze logs from that timeframe and provide recommendations + +![Last X Minutes](https://raw.githubusercontent.com/FrameworkComputer/linux-docs/refs/heads/main/log-helper/images/1.png "Last X Minutes") + +![Finshed scan](https://raw.githubusercontent.com/FrameworkComputer/linux-docs/refs/heads/main/log-helper/images/2.png "Finshed scan") + +**Example use cases:** +- Graphics crashed while gaming +- WiFi suddenly disconnected +- System became unresponsive +- Thermal throttling during heavy workload + +#### 2. Last 24 Hours 📅 +**Best for ongoing or intermittent issues** + +- Select option `2` +- Analyzes the full day of system logs +- Identifies patterns like recurring WiFi drops or thermal cycling + +**Example use cases:** +- Random system freezes throughout the day +- Intermittent USB connection issues +- Gradual performance degradation +- Battery or charging problems + +#### 3. Specific Time Range 🎯 +**Best when you know exactly when the issue occurred** + +- Select option `3` +- Enter start time: `YYYY-MM-DD HH:MM` (24-hour format) +- Enter end time: `YYYY-MM-DD HH:MM` + +**Example:** +- Start: `2025-01-15 14:30` (Jan 15, 2:30 PM) +- End: `2025-01-15 15:00` (Jan 15, 3:00 PM) + +#### 4. Filter Previously Created Log File 🔍 +**For advanced analysis of existing logs** + +- Run after creating a log file with options 1-3 +- Search for specific keywords or phrases +- Creates `filtered_log.txt` with matching entries + +### Understanding the Results + +The diagnostic tool creates `combined_log.txt` with several sections: + +#### 🔧 System Information +- Framework model detection +- Hardware specifications (GPU, WiFi, RAM, storage) +- Current temperatures and power status +- Linux distribution compatibility status + +#### ⚡ Intelligent Recommendations +**Color-coded by severity:** + +- **🔴 IMMEDIATE**: Stop using immediately (dangerous temperatures, hardware faults) +- **🟠 URGENT**: Address soon (GPU crashes, memory issues, storage problems) +- **🟡 IMPORTANT**: Should fix (USB issues, WiFi problems, audio issues) +- **🔵 INFORMATIONAL**: Status updates (distro compatibility, normal thermal behavior) +- **🟢 PREVENTIVE**: Proactive suggestions (elevated temperatures, minor issues) + +#### 📊 Pattern Analysis +- WiFi stability (tracks disconnection frequency) +- USB connection reliability +- GPU stability monitoring +- Thermal management effectiveness + +#### 🎯 Framework-Specific Features + +**Model-Aware Recommendations:** +- Framework Laptop 13: 60W charger verification +- Framework Laptop 16: 180W charger verification + GPU module checks +- Framework Desktop: Power supply diagnostics +- Expansion card troubleshooting + +**Thermal Management:** +- Modern AMD (7040+ series): Higher temperature tolerance (95°C normal) +- Intel processors: Conservative thresholds (80°C watch point) +- Model-specific cooling guidance + +**Hardware Detection:** +- MediaTek WiFi cards (MT7922/MT7925) +- Intel WiFi cards (iwlwifi) +- AMD GPUs (RDNA 2/3) +- Intel integrated graphics +- Framework expansion cards + +### Example Output + +![Details](https://raw.githubusercontent.com/FrameworkComputer/linux-docs/refs/heads/main/log-helper/images/4.png "Details") + + +``` +🖥️ Framework Laptop 16 - checking GPU module and enhanced thermal envelope +🌡️ Current CPU: 67°C via Tctl (Modern AMD: runs hot by design - watch at 90°C, throttles at 95°C, critical at 100°C, emergency at 105°C) +✅ No issues detected + +🔵 INFORMATIONAL Status: +• [DISTRO_COMPATIBILITY] ✅ Your Linux distribution (fedora 42) is officially supported and tested by Framework for your Framework Laptop 16 → You should have the best experience and full hardware support +``` + +### When to Contact Support + +![Intelligent recommendations](https://raw.githubusercontent.com/FrameworkComputer/linux-docs/refs/heads/main/log-helper/images/3.png "Intelligent recommendations") + +**Contact Framework Support if you see:** +- Multiple 🔴 IMMEDIATE or 🟠 URGENT recommendations +- Repeated GPU crashes or system freezes +- Hardware fault messages (Machine Check, MCE errors) +- Persistent thermal issues despite cleaning + +**Include these files with your support ticket:** +- `combined_log.txt` (the full diagnostic report) +- Screenshots of any error messages +- Description of when the issue occurs + +### Quick Commands + +After running the diagnostic: + +```bash +# View full report +cat combined_log.txt + +# View only recommendations +grep -A 20 "INTELLIGENT RECOMMENDATIONS" combined_log.txt + +# Monitor temperatures in real-time +watch -n 2 sensors + +# Check current system status +sensors +``` + +### Framework Resources + +**Support & Documentation:** +- **Support Center**: https://frame.work/support +- **Linux Guides**: https://frame.work/linux +- **Community Forum**: https://community.frame.work/ +- **Knowledge Base**: https://knowledgebase.frame.work/categories/linux-S1IUEcFbkx +- **Linux Tools and Scripts**: https://knowledgebase.frame.work/linux-on-framework-tools-and-scripts-rymax1Jdyg +- **Framework Guides**: https://guides.frame.work/ +- **Enhanced WiFi Analyzer**: https://github.com/FrameworkComputer/linux-docs/tree/main/Enhanced-WiFi-Analyzer + +### Advanced Information + +- [Deep dive into how it works](https://github.com/FrameworkComputer/linux-docs/blob/main/log-helper/how-it-works.md#how-it-works) +- [Troubleshooting common issues](https://github.com/FrameworkComputer/linux-docs/blob/main/log-helper/how-it-works.md#troubleshooting) + +**Note:** Each diagnostic run overwrites the previous `combined_log.txt` file. Save important reports before running again. diff --git a/misc/LUKS-Keyboard-Layout.md b/misc/LUKS-Keyboard-Layout.md new file mode 100644 index 0000000..451c6b4 --- /dev/null +++ b/misc/LUKS-Keyboard-Layout.md @@ -0,0 +1,94 @@ + +# ⚠️ Known Limitation — LUKS Keyboard Layout + +> ⚠️ **Important for non‑US keyboard users:** +> The keyboard layout selected during installation is **not** used for the LUKS unlock screen. The early‑boot environment defaults to **`en‑US`**. +> As a result, the keys you press may produce different characters when you enter your passphrase at boot. + +--- + +## During Installation +- Choose a passphrase you can type on **both** US‑QWERTY *and* your local layout, **or** +- Use only **A–Z** and **0–9** (keys that do not change position). + +> 💡 **Simplest solution:** use a passphrase made **only** of letters A–Z and/or numbers 0–9. These keys map the same on nearly every layout and will always work at the unlock prompt. + +--- + +## After Installation (first boot) + +> ✅ **Skip the entire section below** if you used only A–Z/0‑9 **and** you can already unlock successfully. + +### ✅ Fedora Atomic desktops +*(Silverblue / Kinoite / Bazzite / Bluefin …)* + + # 1 Write your keymap + echo 'KEYMAP=de' | sudo tee /etc/vconsole.conf + + # 2 Track that file so every future deployment includes it + sudo rpm-ostree initramfs-etc --track=/etc/vconsole.conf + + # 3 Rebuild the initramfs now *and* enable automatic rebuilds + sudo rpm-ostree initramfs --enable + + # 4 (Optional but harmless) force a dracut run on the current deployment + sudo dracut -f + + # 5 Reboot + sudo reboot + +--- + +### ✅ Fedora Workstation / Server (traditional RPM systems) + + sudo nano /etc/vconsole.conf # add or edit: KEYMAP=de + sudo dracut -fv --regenerate-all # rebuild every installed kernel + sudo reboot + +--- + +### ✅ Ubuntu & Debian‑based systems + + sudo nano /etc/default/keyboard # e.g. XKBLAYOUT="de" + sudo dpkg-reconfigure keyboard-configuration # interactive; rebuilds current initrd + sudo update-initramfs -u -k all # rebuild all other initrds + sudo reboot + +--- + +### ✅ Arch Linux & derivatives + + echo 'KEYMAP=de' | sudo tee /etc/vconsole.conf + sudo sed -i 's/^HOOKS=.*/HOOKS=(base udev autodetect modconf kms keyboard keymap encrypt filesystems)/' /etc/mkinitcpio.conf + sudo mkinitcpio -P # rebuild every preset + sudo reboot + +--- + +## 🛠️ Emergency Unlock (if the keymap is wrong) + +### Fedora / Atomic +1. At the GRUB menu press **e**. +2. Append `rd.vconsole.keymap=de` to the end of the `linux` line. +3. Boot with **Ctrl + X** or **F10**. +4. After login, apply the permanent fix above. + +### Ubuntu family + + # At GRUB choose “Recovery Mode → root shell” + mount -o remount,rw / + nano /etc/default/keyboard # set XKBLAYOUT="de" + update-initramfs -u -k all + reboot + +--- + +## 📘 Helpful Commands + + # List available keymaps + localectl list-keymaps | grep -i + + # Verify your map is embedded in the current initrd + lsinitramfs /boot/initrd.img-$(uname -r) | grep kmap + +Once these steps are complete, your chosen layout is active at the LUKS prompt, early TTYs, and Plymouth. Your desktop environment continues to use the layout configured in GNOME, KDE, etc. diff --git a/misc/audio-diagnostic/audio-diagnostic.sh b/misc/audio-diagnostic/audio-diagnostic.sh new file mode 100644 index 0000000..3e0d956 --- /dev/null +++ b/misc/audio-diagnostic/audio-diagnostic.sh @@ -0,0 +1,1033 @@ +#!/usr/bin/env bash +# audio-diagnostic.sh — Universal Linux audio diagnostic (Ubuntu/Fedora/Debian) +# Works with PipeWire, PulseAudio, and mixed configurations +# Read-only: makes NO changes. Shows audio routing, profiles, and troubleshooting info. + +set -uo pipefail + +SINCE="${SINCE:-45 minutes ago}" +DO_TEST=0 +VERBOSE=0 + +while [[ $# -gt 0 ]]; do + case "$1" in + -t|--test) DO_TEST=1; shift ;; + --since) SINCE="${2:-45m}"; shift 2 ;; + -v|--verbose) VERBOSE=1; shift ;; + -h|--help) cat < Journal window (e.g. '30 minutes ago', '2 hours ago', 'today') + -v / --verbose Extra detail +EOF + exit 0 ;; + *) echo "Unknown option: $1" >&2; exit 2 ;; + esac +done + +# UI helpers +GREEN="\033[32m"; YELLOW="\033[33m"; RED="\033[31m"; DIM="\033[2m"; BOLD="\033[1m"; CLR="\033[0m" +ok() { echo -e "✅ ${GREEN}$*${CLR}"; } +warn() { echo -e "⚠️ ${YELLOW}$*${CLR}"; } +bad() { echo -e "❌ ${RED}$*${CLR}"; } +info() { echo -e "ℹ️ ${BOLD}$*${CLR}"; } +dim() { echo -e "${DIM}$*${CLR}"; } +sep() { echo -e "${DIM}-------------------------------------------------------------------------------${CLR}"; } + +# Detect audio system and distro +AUDIO_SYSTEM="" +DISTRO_FAMILY="" +HAS_PIPEWIRE=0 +HAS_PULSE=0 +HAS_WIREPLUMBER=0 + +# Check for required commands and detect audio system +if command -v wpctl >/dev/null 2>&1 && command -v pw-cli >/dev/null 2>&1; then + HAS_PIPEWIRE=1 + if command -v wireplumber >/dev/null 2>&1 || systemctl --user is-active wireplumber >/dev/null 2>&1; then + HAS_WIREPLUMBER=1 + fi +fi + +if command -v pactl >/dev/null 2>&1; then + HAS_PULSE=1 +fi + +# Detect distro family +if [[ -f /etc/os-release ]]; then + . /etc/os-release + case "${ID:-}${ID_LIKE:-}" in + *ubuntu*|*debian*) DISTRO_FAMILY="debian" ;; + *fedora*|*rhel*|*centos*) DISTRO_FAMILY="fedora" ;; + *arch*) DISTRO_FAMILY="arch" ;; + *suse*) DISTRO_FAMILY="suse" ;; + *) DISTRO_FAMILY="unknown" ;; + esac +fi + +# Determine primary audio system +if [[ $HAS_PIPEWIRE -eq 1 ]] && systemctl --user is-active pipewire >/dev/null 2>&1; then + if [[ $HAS_WIREPLUMBER -eq 1 ]] && systemctl --user is-active wireplumber >/dev/null 2>&1; then + AUDIO_SYSTEM="pipewire-wireplumber" + else + AUDIO_SYSTEM="pipewire-media-session" + fi +elif [[ $HAS_PULSE -eq 1 ]] && (systemctl --user is-active pulseaudio >/dev/null 2>&1 || pgrep -x pulseaudio >/dev/null 2>&1); then + AUDIO_SYSTEM="pulseaudio" +else + AUDIO_SYSTEM="unknown" +fi + +OS_NAME="$(. /etc/os-release 2>/dev/null && echo "${PRETTY_NAME:-unknown}")" +HOST="$(hostname 2>/dev/null || echo unknown)" +DATE="$(date)" + +echo -e "${BOLD}Linux Audio Diagnostic (Universal • ${OS_NAME})${CLR}" +dim "Host: ${HOST} | Audio: ${AUDIO_SYSTEM} | When: ${DATE} | Journal: ${SINCE}" +sep + +# Track detected issues for conditional output +ISSUE_COUNT=0 +HAS_PROFILE_OFF=0 +HAS_BT_ERRORS=0 +HAS_DUMMY_OUTPUT=0 + +# 1) Core health (services) +echo -e "${BOLD}1) Core audio services health${CLR}" +ALL_OK=1 + +check_service() { + local service="$1" + local description="$2" + local check_type="${3:-systemd}" # systemd or process + + if [[ "$check_type" == "systemd" ]]; then + if systemctl --user is-active "$service" >/dev/null 2>&1; then + ok "$description: active (systemd)" + return 0 + elif systemctl --user status "$service" >/dev/null 2>&1; then + state="$(systemctl --user is-active "$service" 2>/dev/null || echo "inactive")" + bad "$description: $state" + return 1 + fi + fi + + # Fall back to process check + if pgrep -x "$service" >/dev/null 2>&1; then + ok "$description: running (process)" + return 0 + else + # Check if service exists but isn't running + if command -v "$service" >/dev/null 2>&1; then + warn "$description: installed but not running" + else + bad "$description: not found" + fi + return 1 + fi +} + +case "$AUDIO_SYSTEM" in + pipewire-wireplumber) + check_service pipewire "PipeWire core" || ALL_OK=0 + check_service wireplumber "WirePlumber session" || ALL_OK=0 + check_service pipewire-pulse "PulseAudio compatibility" || true # Optional + ;; + pipewire-media-session) + check_service pipewire "PipeWire core" || ALL_OK=0 + check_service pipewire-media-session "Media session manager" || ALL_OK=0 + check_service pipewire-pulse "PulseAudio compatibility" || true # Optional + ;; + pulseaudio) + # PulseAudio might run as systemd service or standalone + if ! check_service pulseaudio "PulseAudio server"; then + ALL_OK=0 + fi + ;; + *) + bad "No recognized audio system detected" + echo "Checking for any audio processes..." + pgrep -la 'pipewire|pulse|jack' || echo " No audio servers running" + ALL_OK=0 + ;; +esac + +[[ $ALL_OK -eq 1 ]] || warn "Inactive audio services can cause missing devices or dummy output." +sep + +# 2) Default devices and routing +echo -e "${BOLD}2) Default devices & active routing${CLR}" + +# Function to get defaults based on audio system +get_audio_defaults() { + DEF_SINK_ID="" + DEF_SINK_NAME="" + DEF_SRC_ID="" + DEF_SRC_NAME="" + + if [[ "$AUDIO_SYSTEM" == "pipewire-wireplumber" ]] && command -v wpctl >/dev/null 2>&1; then + # Use wpctl for PipeWire with WirePlumber + STATUS="$(wpctl status 2>/dev/null || true)" + if [[ -n "$STATUS" ]]; then + # Parse wpctl status (both old and new formats) + # First try old format + DEF_SINK_LINE="$(sed -n 's/^[[:space:]]*Default \(Audio \)\?Sink:[[:space:]]*\([0-9][0-9]*\)[[:space:]]*(\(.*\)).*/\2|\3/p' <<<"$STATUS" | head -n1)" + DEF_SRC_LINE="$(sed -n 's/^[[:space:]]*Default \(Audio \)\?Source:[[:space:]]*\([0-9][0-9]*\)[[:space:]]*(\(.*\)).*/\2|\3/p' <<<"$STATUS" | head -n1)" + + # If old format not found, try new format (asterisk marking) + if [[ -z "$DEF_SINK_LINE" ]]; then + AUDIO_SECTION=$(echo "$STATUS" | sed -n '/^Audio/,/^Video/p') + SINK_SECTION=$(echo "$AUDIO_SECTION" | sed -n '/├─ Sinks:/,/├─ Sources:/p') + DEFAULT_SINK=$(echo "$SINK_SECTION" | grep '│.*\*' | head -n1) + if [[ -n "$DEFAULT_SINK" ]]; then + DEF_SINK_ID=$(echo "$DEFAULT_SINK" | sed -n 's/.*\*[[:space:]]*\([0-9][0-9]*\)\..*/\1/p') + DEF_SINK_NAME=$(echo "$DEFAULT_SINK" | sed -n 's/.*\*[[:space:]]*[0-9][0-9]*\.[[:space:]]*\([^[]*\).*/\1/p' | xargs) + DEF_SINK_LINE="${DEF_SINK_ID}|${DEF_SINK_NAME}" + fi + fi + + if [[ -z "$DEF_SRC_LINE" ]]; then + AUDIO_SECTION=$(echo "$STATUS" | sed -n '/^Audio/,/^Video/p') + SOURCE_SECTION=$(echo "$AUDIO_SECTION" | sed -n '/├─ Sources:/,/├─ Filters:/p') + DEFAULT_SOURCE=$(echo "$SOURCE_SECTION" | grep '│.*\*' | head -n1) + if [[ -z "$DEFAULT_SOURCE" ]]; then + FILTER_SECTION=$(echo "$AUDIO_SECTION" | sed -n '/├─ Filters:/,/└─ Streams:/p') + DEFAULT_SOURCE=$(echo "$FILTER_SECTION" | grep '│.*\*.*\[Audio/Source\]' | head -n1) + fi + if [[ -n "$DEFAULT_SOURCE" ]]; then + DEF_SRC_ID=$(echo "$DEFAULT_SOURCE" | sed -n 's/.*\*[[:space:]]*\([0-9][0-9]*\)\..*/\1/p') + DEF_SRC_NAME=$(echo "$DEFAULT_SOURCE" | sed -n 's/.*\*[[:space:]]*[0-9][0-9]*\.[[:space:]]*\([^[]*\).*/\1/p' | xargs) + DEF_SRC_LINE="${DEF_SRC_ID}|${DEF_SRC_NAME}" + fi + fi + + # Parse the extracted lines + DEF_SINK_ID="${DEF_SINK_LINE%%|*}" + DEF_SINK_NAME="${DEF_SINK_LINE#*|}" + DEF_SRC_ID="${DEF_SRC_LINE%%|*}" + DEF_SRC_NAME="${DEF_SRC_LINE#*|}" + fi + + elif command -v pactl >/dev/null 2>&1; then + # Use pactl for PulseAudio or PipeWire without WirePlumber + # Get default sink + DEF_SINK_NAME="$(pactl info 2>/dev/null | grep "Default Sink:" | cut -d: -f2- | xargs || true)" + if [[ -n "$DEF_SINK_NAME" ]]; then + # Try to get the index + DEF_SINK_ID="$(pactl list short sinks 2>/dev/null | grep -F "$DEF_SINK_NAME" | cut -f1 | head -n1 || true)" + fi + + # Get default source + DEF_SRC_NAME="$(pactl info 2>/dev/null | grep "Default Source:" | cut -d: -f2- | xargs || true)" + if [[ -n "$DEF_SRC_NAME" ]]; then + # Try to get the index + DEF_SRC_ID="$(pactl list short sources 2>/dev/null | grep -F "$DEF_SRC_NAME" | cut -f1 | head -n1 || true)" + fi + fi +} + +get_audio_defaults + +# Report defaults +if [[ -z "${DEF_SINK_ID:-}${DEF_SINK_NAME:-}" ]]; then + bad "No default audio output (sink) configured" +elif [[ "${DEF_SINK_NAME:-}" =~ (auto_null|dummy) ]]; then + bad "Default output is dummy device. Expect silence." + HAS_DUMMY_OUTPUT=1 +else + ok "Default Output: ${DEF_SINK_NAME}${DEF_SINK_ID:+ (ID: $DEF_SINK_ID)}" +fi + +if [[ -z "${DEF_SRC_ID:-}${DEF_SRC_NAME:-}" ]]; then + warn "No default audio input (source) configured" +elif [[ "${DEF_SRC_NAME:-}" =~ (auto_null|dummy) ]]; then + bad "Default input is dummy device" +else + ok "Default Input: ${DEF_SRC_NAME}${DEF_SRC_ID:+ (ID: $DEF_SRC_ID)}" +fi + +# Enhanced technical detail function +describe_device() { + local name="$1" + local type="$2" # sink or source + + if [[ -z "$name" ]] || [[ "$name" =~ (auto_null|dummy) ]]; then + return 0 + fi + + # Add clear section header + echo + if [[ "$type" == "sink" ]]; then + echo -e "${BOLD}Output Device Technical Details:${CLR}" + else + echo -e "${BOLD}Input Device Technical Details:${CLR}" + fi + + if [[ "$type" == "sink" ]]; then + # OUTPUT device technical details + if command -v wpctl >/dev/null 2>&1 && [[ -n "${DEF_SINK_ID:-}" ]]; then + # Get inspection data + local inspect_data=$(wpctl inspect "$DEF_SINK_ID" 2>/dev/null || true) + + # Detect connection type + local connection_type="" + if echo "$inspect_data" | grep -qi "bluez\|bluetooth"; then + connection_type="Bluetooth" + elif echo "$inspect_data" | grep -qi "usb\|USB"; then + connection_type="USB" + elif echo "$inspect_data" | grep -qi "hdmi\|displayport"; then + connection_type="HDMI/DisplayPort" + elif echo "$inspect_data" | grep -qi "alsa.*pci\|pci.*alsa"; then + connection_type="Analog/Built-in" + fi + + [[ -n "$connection_type" ]] && info " • Connection: $connection_type" + + # Extract codec for Bluetooth + if [[ "$connection_type" == "Bluetooth" ]]; then + local codec=$(echo "$inspect_data" | grep -i "codec" | grep -v "available\|supported" | head -n1 | sed 's/.*= *"\?\([^"]*\)"\?$/\1/' | xargs) + if [[ -n "$codec" ]]; then + case "${codec,,}" in + sbc) warn " • Codec: SBC (Basic quality - consider AAC or aptX for better quality)" ;; + aac) ok " • Codec: AAC (Good quality - balanced efficiency)" ;; + aptx*) ok " • Codec: ${codec} (Enhanced quality)" ;; + ldac) ok " • Codec: LDAC (Premium quality - Hi-Res audio)" ;; + *) info " • Codec: $codec" ;; + esac + fi + + # Get battery level using bluetoothctl if available + if command -v bluetoothctl >/dev/null 2>&1; then + # Extract MAC address from bluez node name or properties + local bt_mac="" + if [[ -n "$name" ]] && [[ "$name" =~ bluez ]]; then + # Extract MAC from node name like bluez_output.2C_FD_B3_4A_1F_D0.1 + bt_mac=$(echo "$name" | grep -oE '([0-9A-Fa-f]{2}_){5}[0-9A-Fa-f]{2}' | tr '_' ':') + fi + + if [[ -z "$bt_mac" ]]; then + # Try to get MAC from properties + bt_mac=$(echo "$inspect_data" | grep -i "bluez5.address\|bluetooth.address" | head -n1 | grep -oE '([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}') + fi + + if [[ -n "$bt_mac" ]]; then + # Query battery via bluetoothctl + local bt_info=$(bluetoothctl info "$bt_mac" 2>/dev/null || true) + if [[ -n "$bt_info" ]] && echo "$bt_info" | grep -q "Connected: yes"; then + local raw_batt=$(echo "$bt_info" | awk -F': ' '/Battery Percentage:/ {print $2}' | awk '{print $1}') + if [[ "$raw_batt" =~ ^0x[0-9a-fA-F]+ ]]; then + # Convert hex to decimal + local battery_percent=$((16#${raw_batt#0x})) + if [[ $battery_percent -le 20 ]]; then + bad " • Battery: ${battery_percent}% (Low - charge soon)" + elif [[ $battery_percent -le 50 ]]; then + warn " • Battery: ${battery_percent}% (Moderate)" + else + ok " • Battery: ${battery_percent}%" + fi + elif [[ -n "$raw_batt" ]] && [[ "$raw_batt" =~ ^[0-9]+$ ]]; then + # Already decimal + if [[ $raw_batt -le 20 ]]; then + bad " • Battery: ${raw_batt}% (Low - charge soon)" + elif [[ $raw_batt -le 50 ]]; then + warn " • Battery: ${raw_batt}% (Moderate)" + else + ok " • Battery: ${raw_batt}%" + fi + fi + fi + fi + fi + fi + + # Sample rate detection + local rate=$(echo "$inspect_data" | grep -i "rate" | grep -v "limit\|range" | head -n1 | grep -oE "[0-9]{4,6}" | head -n1) + local format=$(echo "$inspect_data" | grep -i "format" | grep -v "dsp" | head -n1 | sed 's/.*= *"\?\([^"]*\)"\?$/\1/' | xargs) + + # Fallback to pactl if no data + if [[ -z "$rate" ]] && command -v pactl >/dev/null 2>&1; then + local sink_info=$(pactl list sinks 2>/dev/null | awk "/Name:.*${DEF_SINK_NAME//./\\.}/{flag=1} flag && /^$/{flag=0} flag") + if [[ -n "$sink_info" ]]; then + local sample_spec=$(echo "$sink_info" | grep "Sample Specification:" | cut -d: -f2- | xargs) + rate=$(echo "$sample_spec" | grep -oE "[0-9]+Hz" | grep -oE "[0-9]+") + format=$(echo "$sample_spec" | grep -oE "s[0-9]+le|float[0-9]+le") + fi + fi + + if [[ -n "$rate" ]]; then + local display="${rate}Hz${format:+/$format}" + case "$rate" in + 48000|44100) ok " • Sample Rate: $display (Standard quality)" ;; + 96000|192000) ok " • Sample Rate: $display (High resolution)" ;; + *) info " • Sample Rate: $display" ;; + esac + fi + + # Channels + local channels=$(echo "$inspect_data" | grep -i "channel" | head -n1 | grep -oE "[0-9]+" | head -n1) + if [[ -n "$channels" ]]; then + case "$channels" in + 2) ok " • Channels: Stereo" ;; + 1) warn " • Channels: Mono" ;; + 6) info " • Channels: 5.1 Surround" ;; + 8) info " • Channels: 7.1 Surround" ;; + *) info " • Channels: $channels" ;; + esac + fi + + # Latency + local latency=$(echo "$inspect_data" | grep -i "latency\|quantum" | grep -v "limit" | head -n1 | grep -oE "[0-9]+" | head -n1) + if [[ -n "$latency" ]]; then + if [[ $latency -gt 1000 ]]; then + warn " • Latency: ${latency} samples (High - may affect sync)" + else + info " • Latency: ${latency} samples" + fi + fi + + # Volume + local volume_info=$(wpctl get-volume "$DEF_SINK_ID" 2>/dev/null || true) + if [[ -n "$volume_info" ]]; then + local vol_level=$(echo "$volume_info" | grep -oE "[0-9]+\.[0-9]+" | head -n1) + if [[ -n "$vol_level" ]]; then + local vol_percent=$(echo "$vol_level * 100" | bc 2>/dev/null | cut -d. -f1) + [[ -n "$vol_percent" ]] && info " • Volume: ${vol_percent}%" + fi + fi + + elif command -v pactl >/dev/null 2>&1 && [[ -n "${DEF_SINK_NAME:-}" ]]; then + # PulseAudio fallback + local sink_info=$(pactl list sinks 2>/dev/null | sed -n "/Name: $DEF_SINK_NAME/,/^$/p") + local sample_spec=$(echo "$sink_info" | grep "Sample Specification:" | cut -d: -f2- | xargs) + [[ -n "$sample_spec" ]] && info " • Format: $sample_spec" + local volume=$(echo "$sink_info" | grep "Volume:" | head -n1 | grep -oE '[0-9]+%' | head -n1) + [[ -n "$volume" ]] && info " • Volume: $volume" + fi + + elif [[ "$type" == "source" ]]; then + # INPUT device technical details + if command -v wpctl >/dev/null 2>&1 && [[ -n "${DEF_SRC_ID:-}" ]]; then + local inspect_data=$(wpctl inspect "$DEF_SRC_ID" 2>/dev/null || true) + + # Detect connection type + local connection_type="" + if echo "$inspect_data" | grep -qi "bluez\|bluetooth"; then + connection_type="Bluetooth" + elif echo "$inspect_data" | grep -qi "usb\|USB"; then + connection_type="USB" + elif echo "$inspect_data" | grep -qi "webcam\|camera"; then + connection_type="Webcam" + elif echo "$inspect_data" | grep -qi "alsa.*pci\|pci.*alsa"; then + connection_type="Built-in" + fi + + [[ -n "$connection_type" ]] && info " • Mic Type: $connection_type microphone" + + # Bluetooth profile + if [[ "$connection_type" == "Bluetooth" ]]; then + local profile=$(echo "$inspect_data" | grep -i "profile" | grep -v "device\|card" | head -n1 | sed 's/.*= *"\?\([^"]*\)"\?$/\1/' | xargs) + if [[ -n "$profile" ]]; then + case "${profile,,}" in + *a2dp*) warn " • Profile: A2DP (No microphone in this mode)" ;; + *hsp*|*hfp*) info " • Profile: HSP/HFP (Voice quality)" ;; + *) info " • Profile: $profile" ;; + esac + fi + + # Get battery level using bluetoothctl if available + if command -v bluetoothctl >/dev/null 2>&1; then + # Extract MAC address from node name or properties + local bt_mac="" + if [[ -n "$name" ]] && [[ "$name" =~ bluez ]]; then + # Extract MAC from node name like bluez_input.2C_FD_B3_4A_1F_D0.0 + bt_mac=$(echo "$name" | grep -oE '([0-9A-Fa-f]{2}_){5}[0-9A-Fa-f]{2}' | tr '_' ':') + fi + + if [[ -z "$bt_mac" ]]; then + # Try to get MAC from properties + bt_mac=$(echo "$inspect_data" | grep -i "bluez5.address\|bluetooth.address" | head -n1 | grep -oE '([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}') + fi + + if [[ -n "$bt_mac" ]]; then + # Query battery via bluetoothctl + local bt_info=$(bluetoothctl info "$bt_mac" 2>/dev/null || true) + if [[ -n "$bt_info" ]] && echo "$bt_info" | grep -q "Connected: yes"; then + local raw_batt=$(echo "$bt_info" | awk -F': ' '/Battery Percentage:/ {print $2}' | awk '{print $1}') + if [[ "$raw_batt" =~ ^0x[0-9a-fA-F]+ ]]; then + # Convert hex to decimal + local battery_percent=$((16#${raw_batt#0x})) + if [[ $battery_percent -le 20 ]]; then + bad " • Battery: ${battery_percent}% (Low - charge soon)" + elif [[ $battery_percent -le 50 ]]; then + warn " • Battery: ${battery_percent}% (Moderate)" + else + ok " • Battery: ${battery_percent}%" + fi + elif [[ -n "$raw_batt" ]] && [[ "$raw_batt" =~ ^[0-9]+$ ]]; then + # Already decimal + if [[ $raw_batt -le 20 ]]; then + bad " • Battery: ${raw_batt}% (Low - charge soon)" + elif [[ $raw_batt -le 50 ]]; then + warn " • Battery: ${raw_batt}% (Moderate)" + else + ok " • Battery: ${raw_batt}%" + fi + fi + fi + fi + fi + fi + + # Sample rate + local rate=$(echo "$inspect_data" | grep -i "rate" | grep -v "limit\|range" | head -n1 | grep -oE "[0-9]{4,6}" | head -n1) + if [[ -n "$rate" ]]; then + case "$rate" in + 48000) ok " • Sample Rate: 48kHz (Broadcast quality)" ;; + 44100) info " • Sample Rate: 44.1kHz (Standard quality)" ;; + 16000) warn " • Sample Rate: 16kHz (Voice quality)" ;; + *) info " • Sample Rate: ${rate}Hz" ;; + esac + fi + + # Channels + local channels=$(echo "$inspect_data" | grep -i "channel" | head -n1 | grep -oE "[0-9]+" | head -n1) + if [[ -n "$channels" ]]; then + case "$channels" in + 1) info " • Channels: Mono" ;; + 2) info " • Channels: Stereo" ;; + *) info " • Channels: $channels" ;; + esac + fi + + # DSP detection + if echo "$inspect_data" | grep -qi "echo.cancel\|aec"; then + ok " • DSP: Echo cancellation active" + fi + if echo "$inspect_data" | grep -qi "noise.suppress"; then + ok " • DSP: Noise suppression active" + fi + + # Input gain + local volume_info=$(wpctl get-volume "$DEF_SRC_ID" 2>/dev/null || true) + if [[ -n "$volume_info" ]]; then + local vol_level=$(echo "$volume_info" | grep -oE "[0-9]+\.[0-9]+" | head -n1) + if [[ -n "$vol_level" ]]; then + local vol_percent=$(echo "$vol_level * 100" | bc 2>/dev/null | cut -d. -f1) + [[ -n "$vol_percent" ]] && info " • Input Gain: ${vol_percent}%" + fi + fi + + elif command -v pactl >/dev/null 2>&1 && [[ -n "${DEF_SRC_NAME:-}" ]]; then + # PulseAudio fallback + local source_info=$(pactl list sources 2>/dev/null | sed -n "/Name: $DEF_SRC_NAME/,/^$/p") + local sample_spec=$(echo "$source_info" | grep "Sample Specification:" | cut -d: -f2- | xargs) + [[ -n "$sample_spec" ]] && info " • Format: $sample_spec" + local volume=$(echo "$source_info" | grep "Volume:" | head -n1 | grep -oE '[0-9]+%' | head -n1) + [[ -n "$volume" ]] && info " • Input Level: $volume" + fi + fi +} + +if [[ -n "${DEF_SINK_NAME:-}" ]]; then + describe_device "$DEF_SINK_NAME" "sink" +fi + +if [[ -n "${DEF_SRC_NAME:-}" ]]; then + describe_device "$DEF_SRC_NAME" "source" +fi + +sep + +# 3) Available devices +echo -e "${BOLD}3) Available audio devices${CLR}" + +if [[ "$AUDIO_SYSTEM" == "pipewire-wireplumber" ]] && command -v wpctl >/dev/null 2>&1; then + # Get wpctl status and parse it properly + WPCTL_STATUS="$(wpctl status 2>/dev/null || true)" + + echo "Output devices (Sinks):" + echo "$WPCTL_STATUS" | sed -n '/├─ Sinks:/,/├─ Sources:/p' | grep '│' | grep '[0-9]' | while IFS= read -r line; do + clean_line=$(echo "$line" | sed 's/[│├└─]//g' | sed 's/^[[:space:]]*//') + if echo "$clean_line" | grep -q '\*'; then + clean_line=$(echo "$clean_line" | sed 's/\*//g' | sed 's/^[[:space:]]*//') + echo " • $clean_line ← DEFAULT" + else + echo " • $clean_line" + fi + if echo "$clean_line" | grep -qi 'dummy\|auto_null'; then + echo " ⚠️ Dummy output device" + fi + done || echo " No output devices found" + + echo + echo "Input devices (Sources):" + echo "$WPCTL_STATUS" | sed -n '/├─ Sources:/,/├─ \(Filters:\|Devices:\)/p' | grep '│' | grep '[0-9]' | while IFS= read -r line; do + clean_line=$(echo "$line" | sed 's/[│├└─]//g' | sed 's/^[[:space:]]*//') + if echo "$clean_line" | grep -q '\*'; then + clean_line=$(echo "$clean_line" | sed 's/\*//g' | sed 's/^[[:space:]]*//') + echo " • $clean_line ← DEFAULT" + else + echo " • $clean_line" + fi + if echo "$clean_line" | grep -qi 'dummy\|auto_null'; then + echo " ⚠️ Dummy input device" + fi + done || echo " No input devices found" + + echo + echo "Audio Cards/Devices:" + pw-cli ls Device 2>/dev/null | awk ' + /id [0-9]+/ {id=$2} + /device.description/ { + gsub(/.*= "/,""); gsub(/"$/,""); desc=$0 + } + /device.profile.name/ { + gsub(/.*= "/,""); gsub(/"$/,""); prof=$0 + } + /^}$/ && id { + printf(" • Card %s: %s\n", id, (desc!=""?desc:"unnamed")); + if (prof != "") { + printf(" Profile: %s\n", prof); + if (prof == "off") print " ⚠️ Profile is OFF - no inputs/outputs available from this card" + } + id=desc=prof=""; + } + ' || echo " Unable to list cards" + + PW_DEV="$(pw-cli ls Device 2>/dev/null || true)" + if grep -qiE 'device.profile.name.*"off"' <<<"$PW_DEV"; then + HAS_PROFILE_OFF=1 + fi + +elif command -v pactl >/dev/null 2>&1; then + echo "Output devices (Sinks):" + pactl list sinks 2>/dev/null | awk ' + /^Sink #/ {id=$2; gsub("#","",id)} + /^\tName:/ {name=$2} + /^\tDescription:/ {gsub(/^[[:space:]]*Description:[[:space:]]*/,""); desc=$0} + /^\tState:/ {state=$2} + /^\tVolume:/ {if (match($0, /[0-9]+%/)) vol=substr($0, RSTART, RLENGTH)} + /^\tMute:/ {mute=$2} + /^$/ && id { + printf(" • [%s] %s\n", id, (desc!=""?desc:name)); + printf(" State: %s, Volume: %s, Mute: %s\n", state, vol, mute); + if (name ~ /dummy/) print " ⚠️ Dummy output device" + id=name=desc=state=vol=mute="" + } + ' || echo " No output devices found" + + echo + echo "Input devices (Sources):" + pactl list sources 2>/dev/null | grep -v '\.monitor' | awk ' + /^Source #/ {id=$2; gsub("#","",id)} + /^\tName:/ {name=$2; if (name ~ /\.monitor$/) next} + /^\tDescription:/ {gsub(/^[[:space:]]*Description:[[:space:]]*/,""); desc=$0} + /^\tState:/ {state=$2} + /^\tVolume:/ {if (match($0, /[0-9]+%/)) vol=substr($0, RSTART, RLENGTH)} + /^\tMute:/ {mute=$2} + /^$/ && id && name !~ /\.monitor/ { + printf(" • [%s] %s\n", id, (desc!=""?desc:name)); + printf(" State: %s, Volume: %s, Mute: %s\n", state, vol, mute); + if (name ~ /dummy/) print " ⚠️ Dummy input device" + id=name=desc=state=vol=mute="" + } + ' || echo " No input devices found" + + echo + echo "Sound Cards:" + pactl list cards 2>/dev/null | awk ' + /^Card #/ {id=$2; gsub("#","",id)} + /^\tName:/ {name=$2} + /^\tDriver:/ {driver=$2} + /^\tActive Profile:/ {gsub(/^[[:space:]]*Active Profile:[[:space:]]*/,""); prof=$0} + /^$/ && id { + printf(" • Card %s: %s [driver: %s]\n", id, name, driver); + printf(" Active Profile: %s\n", prof); + if (prof == "off") print " ⚠️ Profile is OFF - no inputs/outputs available from this card" + id=name=driver=prof="" + } + ' || echo " Unable to list cards" + + CARDS="$(pactl list cards 2>/dev/null || true)" + if [[ -n "$CARDS" ]] && grep -q "Active Profile: off" <<<"$CARDS"; then + HAS_PROFILE_OFF=1 + fi + +else + echo "Using ALSA to list devices:" + if command -v aplay >/dev/null 2>&1; then + echo "Playback devices:" + aplay -l 2>/dev/null | while IFS= read -r line; do + echo " $line" + done || echo " No playback devices found" + + echo + echo "Capture devices:" + arecord -l 2>/dev/null | while IFS= read -r line; do + echo " $line" + done || echo " No capture devices found" + else + bad "Cannot list devices - no audio tools available" + echo " Install alsa-utils, pulseaudio-utils, or pipewire-utils" + fi +fi + +sep + +# 3b) System-specific checks +echo -e "${BOLD}3b) System-specific checks${CLR}" + +if [[ "$AUDIO_SYSTEM" == pipewire* ]] && command -v wpctl >/dev/null 2>&1; then + SUSPENDED_COUNT=0 + ALL_NODES=$(wpctl status 2>/dev/null | grep -E '^\s*[0-9]+\.' | sed -n 's/.*\[\?\([0-9][0-9]*\)\].*/\1/p') + for NODE_ID in $ALL_NODES; do + NODE_INFO=$(wpctl inspect "$NODE_ID" 2>/dev/null || true) + if grep -q 'node.state = "suspended"' <<<"$NODE_INFO"; then + NODE_DESC=$(sed -n 's/.*node.description = "\(.*\)".*/\1/p' <<<"$NODE_INFO" | head -n1) + bad "Node $NODE_ID suspended: ${NODE_DESC:-unknown}" + SUSPENDED_COUNT=$((SUSPENDED_COUNT + 1)) + fi + done + + if [[ $SUSPENDED_COUNT -eq 0 ]]; then + ok "No suspended audio nodes detected" + else + warn "Found $SUSPENDED_COUNT suspended node(s)" + dim "Fix: systemctl --user restart ${AUDIO_SYSTEM##*-}" + fi +else + if [[ "$DISTRO_FAMILY" == "debian" ]]; then + if ! groups | grep -q audio; then + warn "User not in 'audio' group - may cause permission issues" + dim "Fix: sudo usermod -a -G audio $USER (then logout/login)" + else + ok "User is in audio group" + fi + + if pgrep -x timidity >/dev/null 2>&1; then + warn "TiMidity++ is running - may block audio devices" + dim "Fix: sudo systemctl stop timidity && sudo systemctl disable timidity" + fi + fi + + if command -v aplay >/dev/null 2>&1; then + ALSA_CARDS="$(aplay -l 2>&1 || true)" + if [[ "$ALSA_CARDS" =~ "no soundcards found" ]]; then + bad "ALSA reports no sound cards found" + dim "Check: lspci -v | grep -i audio" + dim "Check: dmesg | grep -Ei 'snd|hda|audio|firmware'" + else + CARD_COUNT="$(echo "$ALSA_CARDS" | grep -c "^card " || true)" + ok "ALSA detected $CARD_COUNT sound card(s)" + fi + fi +fi + +sep + +# 4) Recent logs +echo -e "${BOLD}4) Recent audio system logs${CLR}" + +check_logs() { + local service="$1" + local error_keywords="error|fail|warn|timeout|dummy|auto_null|suspend" + local lifecycle_keywords="Started|Stopped|Starting|Stopping|Reloading|Reloaded|Activating|Deactivating" + + LOG="$(journalctl --user -u "$service" --since "$SINCE" --no-pager 2>/dev/null || true)" + if [[ -z "$LOG" ]]; then + LOG="$(journalctl -u "$service" --since "$SINCE" --no-pager 2>/dev/null || true)" + fi + + if [[ -n "$LOG" ]]; then + LIFECYCLE_HITS=$(grep -Ei "$lifecycle_keywords" <<<"$LOG" | wc -l | tr -d ' ') + if [[ "$LIFECYCLE_HITS" -gt 0 ]]; then + echo "Service $service lifecycle events:" + echo "$LOG" | grep -Ei "$lifecycle_keywords" | tail -n 5 | while IFS= read -r line; do + timestamp=$(echo "$line" | awk '{print $1, $2, $3}') + event=$(echo "$line" | grep -oE "(Started|Stopped|Starting|Stopping|Reloading|Reloaded|Activating|Deactivating).*" | head -n1) + if [[ -n "$event" ]]; then + if [[ "$event" =~ (Stopped|Stopping|fail) ]]; then + warn " $timestamp: $event" + else + dim " $timestamp: $event" + fi + fi + done + echo + fi + + ERROR_HITS=$(grep -Ei "$error_keywords" <<<"$LOG" | wc -l | tr -d ' ') + if [[ "$ERROR_HITS" -gt 0 ]]; then + echo "Found $ERROR_HITS concerning entries in $service logs:" + echo "$LOG" | grep -Ei "$error_keywords" | tail -n 5 | while IFS= read -r line; do + if echo "$line" | grep -qi "error\|fail\|timeout\|dummy\|auto_null"; then + bad " ${line:0:120}..." + else + warn " ${line:0:120}..." + fi + done + + grep -qi 'bluetooth.*error\|bluez.*fail' <<<"$LOG" && HAS_BT_ERRORS=1 + grep -qi 'auto_null\|dummy' <<<"$LOG" && HAS_DUMMY_OUTPUT=1 + fi + + if [[ $VERBOSE -eq 1 ]] && [[ "$LIFECYCLE_HITS" -gt 0 || "$ERROR_HITS" -gt 0 ]]; then + echo + echo -e "${DIM}Full recent log entries (verbose mode):${CLR}" + echo "$LOG" | tail -n 30 | while IFS= read -r line; do + dim " $line" + done + fi + + [[ "$LIFECYCLE_HITS" -gt 0 || "$ERROR_HITS" -gt 0 ]] && return 0 + fi + return 1 +} + +FOUND_LOGS=0 + +case "$AUDIO_SYSTEM" in + pipewire-wireplumber) + check_logs wireplumber && FOUND_LOGS=1 + check_logs pipewire && FOUND_LOGS=1 + check_logs pipewire-pulse && FOUND_LOGS=1 + ;; + pipewire-media-session) + check_logs pipewire-media-session && FOUND_LOGS=1 + check_logs pipewire && FOUND_LOGS=1 + check_logs pipewire-pulse && FOUND_LOGS=1 + ;; + pulseaudio) + check_logs pulseaudio && FOUND_LOGS=1 + ;; +esac + +if [[ "${DEF_SINK_NAME:-}" =~ (bluetooth|bluez) ]] || [[ "${DEF_SRC_NAME:-}" =~ (bluetooth|bluez) ]]; then + check_logs bluetooth && FOUND_LOGS=1 +fi + +if [[ $FOUND_LOGS -eq 0 ]]; then + ok "No service events or errors in recent logs (last $SINCE)" + dim " To see all logs: journalctl --user -u ${AUDIO_SYSTEM##*-} --since '$SINCE'" +fi + +sep + +# 5) Optional test tone +if [[ $DO_TEST -eq 1 ]]; then + echo -e "${BOLD}5) Audio test${CLR}" + + TEST_SOUND="" + for sound in \ + /usr/share/sounds/freedesktop/stereo/complete.oga \ + /usr/share/sounds/freedesktop/stereo/bell.oga \ + /usr/share/sounds/ubuntu/stereo/bell.ogg \ + /usr/share/sounds/alsa/Front_Center.wav \ + /usr/share/sounds/sound-icons/piano-3.wav; do + if [[ -f "$sound" ]]; then + TEST_SOUND="$sound" + break + fi + done + + if [[ -z "$TEST_SOUND" ]]; then + warn "No test sound file found" + else + echo "Playing test sound: $(basename "$TEST_SOUND")" + + if command -v pw-play >/dev/null 2>&1; then + pw-play "$TEST_SOUND" 2>/dev/null && ok "Test completed via PipeWire" || bad "Test failed" + elif command -v paplay >/dev/null 2>&1; then + paplay "$TEST_SOUND" 2>/dev/null && ok "Test completed via PulseAudio" || bad "Test failed" + elif command -v aplay >/dev/null 2>&1; then + aplay "$TEST_SOUND" 2>/dev/null && ok "Test completed via ALSA" || bad "Test failed" + else + warn "No audio player found (pw-play, paplay, or aplay)" + fi + fi + sep +fi + +# 6) Conclusions +echo -e "${BOLD}Conclusions / Next steps${CLR}" + +SHOWED_ISSUES=0 + +if [[ $ALL_OK -ne 1 ]]; then + bad "Critical: Audio service(s) not running properly" + + case "$AUDIO_SYSTEM" in + pipewire*) + echo " Fix: systemctl --user restart pipewire pipewire-pulse wireplumber" + ;; + pulseaudio) + echo " Fix: systemctl --user restart pulseaudio" + echo " Or: pulseaudio --kill && pulseaudio --start" + ;; + *) + echo " Ubuntu: sudo apt install pulseaudio && systemctl --user start pulseaudio" + echo " Fedora: sudo dnf install pipewire wireplumber && systemctl --user start pipewire" + ;; + esac + echo + SHOWED_ISSUES=1 +fi + +if [[ -z "${DEF_SINK_ID:-}${DEF_SINK_NAME:-}" ]]; then + bad "No audio output configured" + echo " Fix 1: Open Settings → Sound → Output Device" + if command -v pactl >/dev/null 2>&1; then + echo " Fix 2: List devices: pactl list short sinks" + echo " Set default: pactl set-default-sink SINK_NAME" + elif command -v wpctl >/dev/null 2>&1; then + echo " Fix 2: List devices: wpctl status" + echo " Set default: wpctl set-default SINK_ID" + fi + echo + SHOWED_ISSUES=1 +fi + +if [[ $HAS_DUMMY_OUTPUT -eq 1 ]]; then + bad "Audio has fallen back to dummy output" + echo " Fix 1: Restart audio service:" + case "$AUDIO_SYSTEM" in + pipewire*) echo " systemctl --user restart pipewire wireplumber" ;; + pulseaudio) echo " systemctl --user restart pulseaudio" ;; + esac + echo " Fix 2: Check if sound card detected: aplay -l" + echo " Fix 3: Check for missing firmware: dmesg | grep -i firmware" + echo " Fix 4: Ubuntu: sudo apt install linux-modules-extra-\$(uname -r)" + echo + SHOWED_ISSUES=1 +fi + +if [[ $HAS_PROFILE_OFF -eq 1 ]]; then + bad "One or more audio devices have profile OFF" + echo " Fix: Settings → Sound → Device Configuration" + echo " Select 'Analog Stereo Duplex' or appropriate profile" + if command -v pactl >/dev/null 2>&1; then + echo " Or: pactl list cards | grep -E 'Name:|Profiles:|Active Profile:'" + echo " pactl set-card-profile CARD_NAME PROFILE_NAME" + fi + echo + SHOWED_ISSUES=1 +fi + +if [[ $HAS_BT_ERRORS -eq 1 ]]; then + warn "Bluetooth audio errors detected" + echo " Fix 1: Toggle Bluetooth off/on in Settings" + echo " Fix 2: Remove and re-pair the device" + echo " Fix 3: sudo systemctl restart bluetooth" + if [[ "$DISTRO_FAMILY" == "debian" ]]; then + echo " Fix 4: Install codecs: sudo apt install pulseaudio-module-bluetooth" + fi + echo + SHOWED_ISSUES=1 +fi + +if [[ ${SUSPENDED_COUNT:-0} -gt 0 ]]; then + bad "Suspended audio nodes detected" + echo " Quick fix: systemctl --user restart wireplumber" + echo " For Framework laptops: Check for firmware updates" + echo + SHOWED_ISSUES=1 +fi + +if [[ "$DISTRO_FAMILY" == "debian" ]] && [[ $SHOWED_ISSUES -eq 1 ]]; then + echo "Ubuntu-specific fixes to try:" + echo " • Remove speech-dispatcher if not needed:" + echo " sudo apt remove speech-dispatcher" + echo " • Reinstall audio packages:" + echo " sudo apt install --reinstall alsa-base alsa-utils pulseaudio" + echo " • Reset PulseAudio config:" + echo " rm -rf ~/.config/pulse && pulseaudio --kill" + echo +fi + +if [[ $SHOWED_ISSUES -eq 0 ]]; then + ok "No critical issues detected - audio system appears healthy" + echo + echo "Useful commands for managing audio:" + echo "• Open audio settings:" + echo " - GNOME: Settings → Sound" + echo " - KDE: System Settings → Audio" + echo " - Or run: pavucontrol (if installed)" + + if ! command -v pavucontrol >/dev/null 2>&1 && command -v alsamixer >/dev/null 2>&1; then + echo + echo "• For easier volume control, consider installing pavucontrol:" + case "$DISTRO_FAMILY" in + debian) echo " sudo apt install pavucontrol" ;; + fedora) echo " sudo dnf install pavucontrol" ;; + arch) echo " sudo pacman -S pavucontrol" ;; + suse) echo " sudo zypper install pavucontrol" ;; + *) echo " Install 'pavucontrol' using your package manager" ;; + esac + echo " Currently available: alsamixer (terminal-based)" + fi + + echo + echo "• Command-line volume control:" + case "$AUDIO_SYSTEM" in + pipewire-wireplumber) + if command -v wpctl >/dev/null 2>&1; then + echo " wpctl status # Show devices" + echo " wpctl set-volume @DEFAULT_SINK@ 50% # Set to 50%" + echo " wpctl set-volume @DEFAULT_SINK@ 5%+ # Increase 5%" + echo " wpctl set-mute @DEFAULT_SINK@ toggle # Mute/unmute" + elif command -v pactl >/dev/null 2>&1; then + echo " pactl info # Show info" + echo " pactl set-sink-volume @DEFAULT_SINK@ 50% # Set to 50%" + echo " pactl set-sink-mute @DEFAULT_SINK@ toggle # Mute/unmute" + fi + ;; + pipewire-media-session|pulseaudio) + if command -v pactl >/dev/null 2>&1; then + echo " pactl info # Show info" + echo " pactl set-sink-volume @DEFAULT_SINK@ 50% # Set to 50%" + echo " pactl set-sink-volume @DEFAULT_SINK@ +5% # Increase 5%" + echo " pactl set-sink-mute @DEFAULT_SINK@ toggle # Mute/unmute" + fi + ;; + *) + if command -v amixer >/dev/null 2>&1; then + echo " alsamixer # Interactive TUI" + echo " amixer set Master 50% # Set to 50%" + echo " amixer set Master 5%+ # Increase 5%" + echo " amixer set Master toggle # Mute/unmute" + fi + ;; + esac + + echo + echo "• View recent audio logs:" + case "$AUDIO_SYSTEM" in + pipewire-wireplumber) + echo " journalctl --user -u wireplumber -u pipewire --since '5 min ago'" + ;; + pipewire-media-session) + echo " journalctl --user -u pipewire-media-session -u pipewire --since '5 min ago'" + ;; + pulseaudio) + echo " journalctl --user -u pulseaudio --since '5 min ago'" + if [[ -d ~/.config/pulse ]]; then + echo " tail -f ~/.config/pulse/*.log" + fi + ;; + *) + echo " dmesg | grep -i audio # Kernel audio messages" + ;; + esac + + echo + echo "• Test audio output:" + if command -v speaker-test >/dev/null 2>&1; then + echo " speaker-test -t wav -c 2 -l 1 # Play test sound once" + echo " speaker-test -t sine -f 440 -l 1 # Play 440Hz tone once" + else + echo " Install 'alsa-utils' package for speaker-test command" + fi +fi + +echo +ok "Diagnostic complete — no changes were made." diff --git a/misc/audio-diagnostic/readme.md b/misc/audio-diagnostic/readme.md new file mode 100644 index 0000000..6e88f80 --- /dev/null +++ b/misc/audio-diagnostic/readme.md @@ -0,0 +1,272 @@ +# 🔊 Linux Audio Diagnostic Script + +A comprehensive, read-only diagnostic tool for troubleshooting Linux audio issues across PipeWire, PulseAudio, and ALSA configurations. + +- [Fedora Audio Guide](https://knowledgebase.frame.work/en_us/fedora-audio-troubleshooting-guide-BJAe1Kr0o) +- [Ubuntu Audio Guide](https://knowledgebase.frame.work/en_us/ubuntu-audio-issues-Bkw2Wlf2o) + +## ✨ Features + +- **Universal compatibility**: Works with PipeWire (WirePlumber/Media Session), PulseAudio, and ALSA +- **Distro-agnostic**: Supports Ubuntu, Fedora, Debian, Arch, and other major distributions +- **Non-invasive**: Read-only operations, makes no system changes +- **Comprehensive checks**: Services, devices, routing, profiles, and recent logs +- **Smart detection**: Identifies common issues like dummy outputs, suspended nodes, and disabled profiles +- **Test capability**: Optional audio playback test to verify output ++ **Technical detail reporting**: Shows Bluetooth codecs, sample rates, latency, and battery levels ++ **Enhanced Bluetooth support**: Real-time battery monitoring via bluetoothctl + +## 📋 Requirements + +### Core Requirements +- Linux system with systemd +- One of: PipeWire, PulseAudio, or ALSA +- Curl needs to be installed; Ubuntu users must install this themselves + +#### Ubuntu users: + +```sudo apt update && sudo apt install curl -y``` + +### Optional Tools (auto-detected) +- `wpctl` - PipeWire control (for PipeWire systems) +- `pactl` - PulseAudio control +- `aplay` - ALSA utilities +- `journalctl` - System log access ++ `bluetoothctl` - Bluetooth battery monitoring ++ `bc` - Volume percentage calculations + +## 🚀 Quick Start + +### One-line Install & Run + +```bash +curl -s https://raw.githubusercontent.com/FrameworkComputer/linux-docs/refs/heads/main/misc/audio-diagnostic/audio-diagnostic.sh -o audio-diagnostic.sh && chmod +x audio-diagnostic.sh && bash audio-diagnostic.sh +``` + +### Step-by-Step + +```bash +# Download the script +curl -s https://raw.githubusercontent.com/FrameworkComputer/linux-docs/refs/heads/main/misc/audio-diagnostic/audio-diagnostic.sh -o audio-diagnostic.sh + +# Make it executable +chmod +x audio-diagnostic.sh + +# Run basic diagnostic +./audio-diagnostic.sh + +# Run with audio test +./audio-diagnostic.sh -t + +# Check recent issues (last 2 hours) +./audio-diagnostic.sh --since "2 hours ago" +``` + +## 📖 Usage + +```bash +./audio-diagnostic.sh [OPTIONS] +``` + +### Options + +| Option | Description | Example | +|--------|-------------|---------| +| `-t`, `--test` | Play a test sound after diagnostics | `./audio-diagnostic.sh -t` | +| `--since