#!/usr/bin/env bash # What this machine is, asked one yes-or-no question at a time. # # panama-hw laptop && echo "portable" # panama-hw clamshell && panama-lid close # # Every subcommand exits 0 for yes and 1 for no, prints nothing, and answers # correctly on a machine that has none of the hardware in question. That last # part is the whole point: a desktop must be able to ask "am I in clamshell # mode" and get a calm no rather than an error, because the scripts and # services that ask are shared between machines. # # `--json` answers everything at once, for the health page and for contracts. # # Detection reads sysfs directly rather than shelling out to lspci or upower: # lspci touches PCI config space and wakes a runtime-suspended GPU, which is a # real cost to pay for a question asked at every login. # # Paths are overridable (PANAMA_HW_SYS, PANAMA_HW_ACPI) so the contract can # drive fixture trees. Nothing else should set them. set -uo pipefail SYS="${PANAMA_HW_SYS:-/sys}" ACPI="${PANAMA_HW_ACPI:-/proc/acpi}" # SMBIOS chassis types that mean "carried around": Portable, Laptop, Notebook, # Hand Held, Sub Notebook, Tablet, Convertible, Detachable. A machine that # reports something else, or reports nothing, is treated as stationary -- # guessing "laptop" on an unknown chassis would put battery chrome on a desktop. readonly PORTABLE_CHASSIS=" 8 9 10 11 14 30 31 32 " is_laptop() { local type_file="$SYS/class/dmi/id/chassis_type" chassis [[ -r "$type_file" ]] || return 1 chassis="$(cat "$type_file" 2>/dev/null)" || return 1 [[ "$PORTABLE_CHASSIS" == *" $chassis "* ]] } # The first SYSTEM battery, or nothing. Named rather than assumed to be BAT0: # the second battery in a ThinkPad is BAT1, and a machine with only BAT1 # exists. The scope check is what keeps a desktop a desktop: a wireless mouse # or a game controller publishes type=Battery with scope=Device, and counting # one turned a tower into a "laptop" whose battery readout was the mouse's. battery_path() { local supply type scope for supply in "$SYS"/class/power_supply/*; do [[ -r "$supply/type" ]] || continue type="$(cat "$supply/type" 2>/dev/null)" [[ "$type" == "Battery" ]] || continue scope="$(cat "$supply/scope" 2>/dev/null || echo System)" [[ "$scope" == "Device" ]] && continue printf '%s\n' "$supply" return 0 done return 1 } has_battery() { battery_path >/dev/null; } # On wall power. A machine with no mains supply at all and no system battery # is a desktop, and a desktop is always on wall power -- answering "no" there # would make every battery-aware timing apply to a machine that cannot run # out of power. But "no Mains" alone is not "desktop": hardware charged only # over USB-PD exposes type=USB supplies and no Mains at all, and reading that # as permanently-on-AC meant its battery timings never engaged while it ran # down. When no Mains exists but a system battery does, the battery's own # status is the answer: Discharging means battery, everything else means fed. on_ac() { local supply type online found=1 battery status for supply in "$SYS"/class/power_supply/*; do [[ -r "$supply/type" ]] || continue type="$(cat "$supply/type" 2>/dev/null)" [[ "$type" == "Mains" ]] || continue found=0 online="$(cat "$supply/online" 2>/dev/null || echo 0)" [[ "$online" == "1" ]] && return 0 done # Mains exists and none of it is online: genuinely on battery. (( found == 0 )) && return 1 if battery="$(battery_path)"; then status="$(cat "$battery/status" 2>/dev/null || echo Unknown)" [[ "$status" == "Discharging" ]] && return 1 fi return 0 } # ACPI first, logind second. Some platforms expose the lid only as an evdev # switch with no /proc/acpi/button entry; logind watches the switch either # way, so its LidClosed property is the fallback that keeps clamshell # detection honest there. No logind (a container, a test tree) means the # fallback quietly answers open, which is the safe direction. lid_closed() { local state for state in "$ACPI"/button/lid/*/state; do [[ -r "$state" ]] || continue grep -qi closed "$state" && return 0 return 1 done [[ -d "$ACPI/button/lid" ]] && return 1 busctl get-property org.freedesktop.login1 /org/freedesktop/login1 \ org.freedesktop.login1.Manager LidClosed 2>/dev/null | grep -q 'b true' } # A connected output that is not the built-in panel. eDP, LVDS and DSI are the # internal ones; everything else arrived through a cable. has_external_monitor() { local status connector for status in "$SYS"/class/drm/card*-*/status; do [[ -r "$status" ]] || continue [[ "$(cat "$status" 2>/dev/null)" == "connected" ]] || continue connector="$(basename "$(dirname "$status")")" case "$connector" in *eDP*|*LVDS*|*DSI*) continue ;; *) return 0 ;; esac done return 1 } # The one definition the rest of the laptop work hangs on: the lid is shut and # there is still a screen to use. Closing the lid on a dock must not suspend; # closing it on a train must. is_clamshell() { lid_closed && has_external_monitor; } has_touchpad() { local name for name in "$SYS"/class/input/*/name; do [[ -r "$name" ]] || continue grep -qi touchpad "$name" && return 0 done return 1 } # Vendor 0x10de on a display-class device. Read from sysfs rather than lspci # so an idle discrete GPU is not woken to answer. has_nvidia() { local device vendor class for device in "$SYS"/bus/pci/devices/*; do [[ -r "$device/vendor" && -r "$device/class" ]] || continue vendor="$(cat "$device/vendor" 2>/dev/null)" [[ "$vendor" == "0x10de" ]] || continue class="$(cat "$device/class" 2>/dev/null)" [[ "$class" == 0x03* ]] && return 0 done return 1 } answer() { "$1" && printf 'true' || printf 'false'; } cmd_json() { printf '{"laptop":%s,"battery":%s,"ac":%s,"lidClosed":%s,"externalMonitor":%s,"clamshell":%s,"touchpad":%s,"nvidia":%s}\n' \ "$(answer is_laptop)" "$(answer has_battery)" "$(answer on_ac)" \ "$(answer lid_closed)" "$(answer has_external_monitor)" \ "$(answer is_clamshell)" "$(answer has_touchpad)" "$(answer has_nvidia)" } case "${1:-}" in laptop) is_laptop ;; battery) has_battery ;; battery-path) battery_path ;; ac) on_ac ;; lid-closed) lid_closed ;; external-monitor) has_external_monitor ;; clamshell) is_clamshell ;; touchpad) has_touchpad ;; nvidia) has_nvidia ;; --json) cmd_json ;; -h|--help|"") cat <<'USAGE' usage: panama-hw Exits 0 for yes, 1 for no, and prints nothing. laptop a portable chassis battery a battery is present battery-path print the first battery's sysfs path (0 if found) ac on wall power (a machine with no mains is always yes) lid-closed the lid is shut external-monitor a connected output that is not the built-in panel clamshell lid shut AND an external monitor: docked, keep working touchpad a touchpad is present nvidia an NVIDIA display device is present --json every answer at once USAGE ;; *) printf 'panama-hw: unknown predicate: %s\n' "$1" >&2; exit 2 ;; esac