From 6526403c1a076f3ddbfb03e8620fc82837bab3cb Mon Sep 17 00:00:00 2001 From: Thomas Koch Date: Sun, 16 Jun 2024 20:01:05 +0200 Subject: [PATCH] bat.d driver: Framework laptops * Requires framework_laptop kernel module (currently out-of-tree) * Stop threshold only * Force discharge not supported * Requires Framework Laptop 13/16 Intel/AMD * charge_control_end_threshold: - Valid values: 1..100 - Default value: 100 Credits: * https://github.com/linrunner/TLP/pull/728 Reference: * https://github.com/DHowett/framework-laptop-kmod/tree/main --- bat.d/55-framework | 515 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 515 insertions(+) create mode 100644 bat.d/55-framework diff --git a/bat.d/55-framework b/bat.d/55-framework new file mode 100644 index 00000000..95464fef --- /dev/null +++ b/bat.d/55-framework @@ -0,0 +1,515 @@ +#!/bin/sh +# 55-framework - Battery Plugin for Framework laptops +# Requires the currently out-of-tree module framework_laptop at https://github.com/DHowett/framework-laptop-kmod +# +# Copyright (c) 2024 Thomas Koch and others. +# SPDX-License-Identifier: GPL-2.0-or-later + +# Needs: tlp-func-base, 35-tlp-func-batt, tlp-func-stat + +# --- Hardware Detection + +readonly BATDRV_FRAMEWORK_MD=/sys/module/framework_laptop + +batdrv_is_framework () { + # check if vendor specific kernel module is loaded + # rc: 0=ok, 1=other hardware + [ -d $BATDRV_FRAMEWORK_MD ] +} + +# --- Plugin API functions + +batdrv_init () { + # detect hardware and initialize driver + # rc: 0=matching hardware detected/1=not detected/2=no batteries detected + # retval: $_batdrv_plugin, $_batdrv_kmod + # + # 1. check for native kernel acpi (out-of-tree module required) + # --> retval $_natacpi: + # 0=thresholds/ + # 32=disabled/ + # 128=no kernel support/ + # 254=laptop not supported + # + # 2. determine method for + # reading battery data --> retval $_bm_read, + # reading/writing charging thresholds --> retval $_bm_thresh, + # reading/writing force discharge --> retval $_bm_dischg: + # none/natacpi + # + # 3. define sysfile basenames for natacpi + # stop threshold --> retval $_bn_stop, + # + # 4. determine present batteries + # list of batteries (space separated) --> retval $_batteries; + # + # 5. define charge threshold defaults + # stop threshold --> retval $_bt_def_stop; + + _batdrv_plugin="framework" + _batdrv_kmod="framework_laptop" # kernel module for natacpi + + # check plugin simulation override and denylist + if [ -n "$X_BAT_PLUGIN_SIMULATE" ]; then + if [ "$X_BAT_PLUGIN_SIMULATE" = "$_batdrv_plugin" ]; then + echo_debug "bat" "batdrv_init.${_batdrv_plugin}.simulate" + else + echo_debug "bat" "batdrv_init.${_batdrv_plugin}.simulate_skip" + return 1 + fi + elif wordinlist "$_batdrv_plugin" "$X_BAT_PLUGIN_DENYLIST"; then + echo_debug "bat" "batdrv_init.${_batdrv_plugin}.denylist" + return 1 + else + # check if hardware matches + if ! batdrv_is_framework; then + echo_debug "bat" "batdrv_init.${_batdrv_plugin}.no_match" + return 1 + fi + fi + + # presume no features at all + _natacpi=128 + _bm_read="natacpi" + _bm_thresh="none" + _bm_dischg="none" + _bn_start="" + _bn_stop="" + _bn_dischg="" + _batteries="" + # vendor default + _bt_def_stop=100 + + # iterate batteries and check for native kernel ACPI + local bd bs + local done=0 + for bd in "$ACPIBATDIR"/BAT[01]; do + if [ "$(read_sysf "$bd/present")" = "1" ]; then + # record detected batteries and directories + bs=${bd##/*/} + if [ -n "$_batteries" ]; then + _batteries="$_batteries $bs" + else + _batteries="$bs" + fi + # skip natacpi detection for 2nd and subsequent batteries + [ $done -eq 1 ] && continue + + done=1 + if [ "$NATACPI_ENABLE" = "0" ]; then + # natacpi disabled in configuration --> skip actual detection + _natacpi=32 + continue + fi + + if [ -f "$bd/charge_control_end_threshold" ]; then + # threshold sysfiles exist + _bn_stop="charge_control_end_threshold" + _natacpi=254 + else + # nothing detected + _natacpi=254 + continue + fi + + if readable_sysf "$bd/$_bn_stop"; then + # threshold sysfiles are actually readable + _natacpi=0 + _bm_thresh="natacpi" + fi + fi + done + + # quit if no battery detected, there is no point in activating the plugin + if [ -z "$_batteries" ]; then + echo_debug "bat" "batdrv_init.${_batdrv_plugin}.no_batteries" + return 2 + fi + + # shellcheck disable=SC2034 + _batdrv_selected=$_batdrv_plugin + echo_debug "bat" "batdrv_init.${_batdrv_plugin}: batteries=$_batteries; natacpi=$_natacpi; thresh=$_bm_thresh; bn_start=$_bn_start; bn_stop=$_bn_stop; dischg=$_bm_dischg; bn_dischg=$_bn_dischg" + return 0 +} + +batdrv_select_battery () { + # determine battery acpidir and sysfiles + # $1: BAT0/BAT1/DEF + # global params: $_batdrv_plugin, $_batteries, $_bn_start, $_bn_stop, $_bn_dischg + # rc: 0=bat exists/1=bat non-existent + # retval: $_bat_str: BAT0/BAT1/; + # $_bt_cfg_bat: config suffix (BAT0/BAT1); + # $_bd_read: directory with battery data sysfiles; + # $_bf_stop: sysfile for stop threshold; + # prerequisite: batdrv_init() + + # defaults + _bat_str="" # no bat + _bt_cfg_bat="" + _bd_read="" # no directory + _bf_stop="" + + local bat="$1" + + # convert battery param to uppercase + bat="$(printf '%s' "$bat" | tr "[:lower:]" "[:upper:]")" + + # validate battery param + case "$bat" in + DEF) # 1st battery is default + _bat_str="${_batteries%% *}" + ;; + + *) + if wordinlist "$bat" "$_batteries"; then + _bat_str="$bat" + else + # battery not present --> quit + echo_debug "bat" "batdrv.${_batdrv_plugin}.select_battery($1).not_present" + return 1 + fi + ;; + esac + + # config suffix equals battery name + _bt_cfg_bat="$_bat_str" + + # determine natacpi sysfile + _bd_read="$ACPIBATDIR/$_bat_str" + if [ "$_bm_thresh" = "natacpi" ]; then + _bf_stop="$ACPIBATDIR/$_bat_str/$_bn_stop" + fi + + echo_debug "bat" "batdrv.${_batdrv_plugin}.select_battery($1): bat_str=$_bat_str; cfg=$_bt_cfg_bat; bd_read=$_bd_read; bf_stop=$_bf_stop" + return 0 +} + +batdrv_read_threshold () { + # read and print charge threshold (stop only) + # $1: 0=api/1=tlp-stat output + # global params: $_batdrv_plugin, $_bm_thresh, $_bf_start, $_bf_stop + # out: + # - api: 1..100/"" on error + # - tlp-stat: 1..100/"(not available)" on error + # rc: 0=ok/4=read error/255=no api + # prerequisite: batdrv_init(), batdrv_select_battery() + + local out rc=0 + + out="$X_THRESH_SIMULATE_STOP" + if [ -n "$out" ]; then + printf "%s" "$out" + echo_debug "bat" "batdrv.${_batdrv_plugin}.read_threshold($1).simulate: bm_thresh=$_bm_thresh; bf_stop=$_bf_stop; out=$out; rc=$rc" + return 0 + fi + + if [ "$_bm_thresh" = "natacpi" ]; then + if ! out=$(read_sysf "$_bf_stop"); then + # not readable/non-existent + if [ "$1" != "1" ]; then + out="" + else + out="(not available)" + fi + rc=4 + fi + else + # no threshold api + if [ "$1" = "1" ]; then + out="(not available)" + fi + rc=255 + fi + + # "return" threshold + if [ "$X_THRESH_SIMULATE_READERR" != "1" ]; then + printf "%s" "$out" + else + if [ "$1" = "1" ]; then + printf "(not available)\n" + fi + rc=4 + fi + + echo_debug "bat" "batdrv.${_batdrv_plugin}.read_threshold($1): bm_thresh=$_bm_thresh; bf_stop=$_bf_stop; out=$out; rc=$rc" + return $rc +} + +batdrv_write_thresholds () { + # write charge thresholds for a battery + # use pre-determined method and sysfiles from global parms + # $1: new start threshold -- unused dummy for plugin api compatibility + # $2: new stop threshold 1..100/DEF(default) + # $3: 0=quiet/1=output parameter errors/2=output progress and errors + # $4: non-empty string indicates thresholds stem from configuration + # global params: $_batdrv_plugin, $_bm_thresh, $_bat_str, $_bt_cfg_bat, $_bf_stop + # rc: 0=ok/ + # 1=not configured/ + # 2=threshold out of range or non-numeric/ + # 4=threshold read error/ + # 5=threshold write error + # prerequisite: batdrv_init(), batdrv_select_battery() + + local new_stop=${2:-} + local verb=${3:-0} + + # insert defaults + [ "$new_stop" = "DEF" ] && new_stop=$_bt_def_stop + + # --- validate thresholds + if [ -n "$4" ] && [ -z "$new_stop" ]; then + # do nothing if unconfigured + echo_debug "bat" "batdrv.${_batdrv_plugin}.write_thresholds($2, $3, $4).not_configured: bat=$_bat_str; cfg=$_bt_cfg_bat" + return 1 + fi + + # stop: check for 3 digits max, ensure min 1 / max 100 + if ! is_uint "$new_stop" 3 || \ + ! is_within_bounds "$new_stop" 1 100; then + # threshold out of range + echo_debug "bat" "batdrv.${_batdrv_plugin}.write_thresholds($1, $2, $3, $4).invalid_stop: bat=$_bat_str; cfg=$_bt_cfg_bat" + case $verb in + 1) + if [ -n "$4" ]; then + echo_message "Error in configuration at STOP_CHARGE_THRESH_${_bt_cfg_bat}=\"${new_stop}\": not specified, invalid or out of range (1..100). Battery skipped." + fi + ;; + + 2) + if [ -n "$4" ]; then + cprintf "" "Error in configuration at STOP_CHARGE_THRESH_%s=\"%s\": not specified, invalid or out of range (1..100). Aborted.\n" "$_bt_cfg_bat" "$new_stop" 1>&2 + else + cprintf "" "Error: stop charge threshold (%s) for %s is not specified, invalid or out of range (1..100). Aborted.\n" "$new_stop" "$_bat_str" 1>&2 + fi + ;; + esac + return 2 + fi + + # write new threshold + if [ "$verb" = "2" ]; then + printf "Setting temporary charge threshold for %s:\n" "$_bat_str" 1>&2 + fi + + local rc=0 + write_sysf "$new_stop" "$_bf_stop" || rc=5 + echo_debug "bat" "batdrv.${_batdrv_plugin}.write_thresholds($2, $3, $4).write: bat=$_bat_str; cfg=$_bt_cfg_bat; new=$new_stop; rc=$rc" + case $verb in + 2) + if [ $rc -eq 0 ]; then + printf " stop = %3d\n" "$new_stop" 1>&2 + else + cprintf "err" " stop = %3d (Error: write failed)\n" "$new_stop" 1>&2 + fi + ;; + 1) + if [ $rc -gt 0 ]; then + echo_message "Error: writing stop charge threshold for $_bat_str failed." + fi + ;; + esac + + return $rc +} + +batdrv_chargeonce () { + # function not implemented + # global param: $_batdrv_plugin + # prerequisite: batdrv_init() + + echo_debug "bat" "batdrv.${_batdrv_plugin}.charge_once.not_implemented" + return 255 +} + +batdrv_apply_configured_thresholds () { + # apply configured stop thresholds from configuration to all batteries + # - called for bg tasks tlp init [re]start/auto and tlp start + # output parameter errors only + # prerequisite: batdrv_init() + + local bat stop_thresh + + for bat in BAT0 BAT1; do + if batdrv_select_battery "$bat"; then + eval stop_thresh="\$STOP_CHARGE_THRESH_${_bt_cfg_bat}" + batdrv_write_thresholds "" "$stop_thresh" 1 1 + fi + done + + return 0 +} + +batdrv_read_force_discharge () { + # function not implemented + # global param: $_batdrv_plugin + # prerequisite: batdrv_init() + + echo_debug "bat" "batdrv.${_batdrv_plugin}.read_force_discharge.not_implemented" + return 255 +} + +batdrv_write_force_discharge () { + # function not implemented + # global param: $_batdrv_plugin + # prerequisite: batdrv_init() + + echo_debug "bat" "batdrv.${_batdrv_plugin}.write_force_discharge.not_implemented" + return 255 +} + +batdrv_cancel_force_discharge () { + # function not implemented + # global param: $_batdrv_plugin + # prerequisite: batdrv_init() + + echo_debug "bat" "batdrv.${_batdrv_plugin}.cancel_force_discharge.not_implemented" + return 255 +} + +batdrv_force_discharge_active () { + # function not implemented + # global param: $_batdrv_plugin + # prerequisite: batdrv_init() + + echo_debug "bat" "batdrv.${_batdrv_plugin}.force_discharge_active.not_implemented" + return 255 +} + +batdrv_discharge () { + # function not implemented + # global param: $_batdrv_plugin + # prerequisite: batdrv_init() + + # Important: release lock from caller + unlock_tlp tlp_discharge + + echo_debug "bat" "batdrv.${_batdrv_plugin}.discharge.not_implemented" + return 255 +} + +batdrv_show_battery_data () { + # output battery status + # $1: 1=verbose + # global params: $_batdrv_plugin, $_batteries, $_batdrv_kmod, $_bd_read, $_bf_start, $_bf_stop, $_bf_dischg + # prerequisite: batdrv_init() + + local verbose=${1:-0} + + printf "+++ Battery Care\n" + printf "Plugin: %s\n" "$_batdrv_plugin" + + if [ "$_bm_thresh" != "none" ]; then + cprintf "success" "Supported features: charge threshold\n" + else + cprintf "warning" "Supported features: none available\n" + fi + + printf "Driver usage:\n" + # native kernel ACPI battery API + case $_natacpi in + 0) cprintf "success" "* natacpi (%s) = active (charge thresholds)\n" "$_batdrv_kmod" ;; + 32) cprintf "notice" "* natacpi (%s) = inactive (disabled by configuration)\n" "$_batdrv_kmod" ;; + 128) cprintf "err" "* natacpi (%s) = inactive (no kernel support)\n" "$_batdrv_kmod" ;; + 254) cprintf "warning" "* natacpi (%s) = inactive (laptop not supported)\n" "$_batdrv_kmod" ;; + *) cprintf "err" "* natacpi (%s) = unknown status\n" "$_batdrv_kmod" ;; + esac + if [ "$_bm_thresh" != "none" ]; then + printf "Parameter value ranges:\n" + printf "* STOP_CHARGE_THRESH_BAT0/1: 1..100(default)\n" + fi + printf "\n" + + # -- show battery data + local bat + local bcnt=0 + local ed ef en + local efsum=0 + local ensum=0 + + for bat in $_batteries; do # iterate batteries + batdrv_select_battery "$bat" + + printf "+++ Battery Status: %s\n" "$bat" + + printparm "%-59s = ##%s##" "$_bd_read/manufacturer" + printparm "%-59s = ##%s##" "$_bd_read/model_name" + + print_battery_cycle_count "$_bd_read/cycle_count" "$(read_sysf "$_bd_read/cycle_count")" + + if [ -f "$_bd_read/energy_full" ]; then + printparm "%-59s = ##%6d## [mWh]" "$_bd_read/energy_full_design" "" 000 + printparm "%-59s = ##%6d## [mWh]" "$_bd_read/energy_full" "" 000 + printparm "%-59s = ##%6d## [mWh]" "$_bd_read/energy_now" "" 000 + printparm "%-59s = ##%6d## [mW]" "$_bd_read/power_now" "" 000 + + # store values for charge / capacity calculation below + ed=$(read_sysval "$_bd_read/energy_full_design") + ef=$(read_sysval "$_bd_read/energy_full") + en=$(read_sysval "$_bd_read/energy_now") + efsum=$((efsum + ef)) + ensum=$((ensum + en)) + + elif [ -f "$_bd_read/charge_full" ]; then + printparm "%-59s = ##%6d## [mAh]" "$_bd_read/charge_full_design" "" 000 + printparm "%-59s = ##%6d## [mAh]" "$_bd_read/charge_full" "" 000 + printparm "%-59s = ##%6d## [mAh]" "$_bd_read/charge_now" "" 000 + printparm "%-59s = ##%6d## [mA]" "$_bd_read/current_now" "" 000 + + # store values for charge / capacity calculation below + ed=$(read_sysval "$_bd_read/charge_full_design") + ef=$(read_sysval "$_bd_read/charge_full") + en=$(read_sysval "$_bd_read/charge_now") + efsum=$((efsum + ef)) + ensum=$((ensum + en)) + + else + ed=0 + ef=0 + en=0 + fi + + print_batstate "$_bd_read/status" + printf "\n" + + if [ "$verbose" -eq 1 ]; then + printparm "%-59s = ##%6s## [mV]" "$_bd_read/voltage_min_design" "" 000 + printparm "%-59s = ##%6s## [mV]" "$_bd_read/voltage_now" "" 000 + printf "\n" + fi + + # --- show battery features: thresholds + if [ "$_bm_thresh" = "natacpi" ]; then + printf "%-59s = %6s [%%]\n" "$_bf_stop" "$(batdrv_read_threshold 1)" + printf "\n" + fi + + # --- show charge level (SOC) and capacity + lf=0 + if [ "$ef" -ne 0 ]; then + perl -e 'printf ("%-59s = %6.1f [%%]\n", "Charge", 100.0 * '"$en"' / '"$ef"');' + lf=1 + fi + if [ "$ed" -ne 0 ]; then + perl -e 'printf ("%-59s = %6.1f [%%]\n", "Capacity", 100.0 * '"$ef"' / '"$ed"');' + lf=1 + fi + [ "$lf" -gt 0 ] && printf "\n" + + bcnt=$((bcnt+1)) + + done # for bat + + if [ $bcnt -gt 1 ] && [ $efsum -ne 0 ]; then + # more than one battery detected --> show charge total + perl -e 'printf ("%-59s = %6.1f [%%]\n", "+++ Charge total", 100.0 * '"$ensum"' / '"$efsum"');' + printf "\n" + fi + + return 0 +} + +batdrv_recommendations () { + # no recommendations + + return 0 +}