diff --git a/bat.d/41-framework b/bat.d/41-framework new file mode 100644 index 00000000..6093921d --- /dev/null +++ b/bat.d/41-framework @@ -0,0 +1,618 @@ +#!/bin/sh +# Battery Plugin for Framework laptops +# +# To make this work the "standard" cros charge control has to be enabled. +# This can be done by passing the probe_with_fwk_charge_control to the +# cros_charge_control module + +# /etc/modprobe.d/framework.conf +# options cros_charge_control probe_with_fwk_charge_control +# +# Copyright (c) 2024 Patrick McLean 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/class/power_supply/BAT1/model_name + +batdrv_is_framework () { + # check if vendor specific kernel module is loaded + # rc: 0=ok, 1=other hardware + case "$(read_sysf $BATDRV_FRAMEWORK_MD)" in + FRA*) return 0;; + esac + + return 1 +} + +# --- 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 (Linux 5.4 or higher 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 + # shellcheck disable=SC2034 + _bm_read="natacpi" + _bm_thresh="none" + # shellcheck disable=SC2034 + _bm_dischg="none" + _bn_start="" + _bn_stop="" + _batteries="" + _bt_def_start=50 + _bt_def_stop=60 + + # 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_start_threshold" ] \ + && [ -f "$bd/charge_control_end_threshold" ]; then + # threshold sysfiles exist + _bn_start="charge_control_start_threshold" + _bn_stop="charge_control_end_threshold" + _natacpi=254 + else + # nothing detected + _natacpi=254 + continue + fi + + if readable_sysf "$bd/$_bn_start" \ + && 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;" + return 0 +} + +batdrv_select_battery () { + # determine battery acpidir and sysfiles + # $1: BAT0/BAT1/DEF + # # rc: 0=bat exists/1=bat non-existent + # retval: $_bat_str: BAT0/BAT1/BATT; + # $_bd_read: directory with battery data sysfiles; + # $_bf_start: sysfile for stop threshold; + # $_bf_stop: sysfile for stop threshold; + # prerequisite: batdrv_init() + + # defaults + _bat_str="" # no bat + _bd_read="" # no directory + _bf_start="" + _bf_stop="" + + # validate battery param + local bs + case $1 in + DEF) # 1st battery is default + _bat_str="${_batteries%% *}" + ;; + + *) + if wordinlist "$1" "$_batteries"; then + _bat_str=$1 + else + # battery not present --> quit + echo_debug "bat" "batdrv.${_batdrv_plugin}.select_battery($1).not_present" + return 1 + fi + ;; + esac + + # determine natacpi sysfiles + _bd_read="$ACPIBATDIR/$_bat_str" + if [ "$_bm_thresh" = "natacpi" ]; then + _bf_start="$ACPIBATDIR/$_bat_str/$_bn_start" + _bf_stop="$ACPIBATDIR/$_bat_str/$_bn_stop" + fi + + echo_debug "bat" "batdrv.${_batdrv_plugin}.select_battery($1): bat_str=$_bat_str; bd_read=$_bd_read; bf_start=$_bf_start; bf_stop=$_bf_stop" + return 0 +} + +batdrv_read_threshold () { + # read and print charge threshold + # $1: start/stop + # $2: 0=api/1=tlp-stat output + # global param: $_bm_thresh, $_bf_start, $_bf_stop + # out: + # - api: 0..100/"" on error + # - tlp-stat: 0..100/"(not available)" on error + # rc: 0=ok/4=read error/255=no api + # prerequisite: batdrv_init(), batdrv_select_battery() + + local bf out="" rc=0 + + case $1 in + start) out="$X_THRESH_SIMULATE_START" ;; + stop) out="$X_THRESH_SIMULATE_STOP" ;; + esac + if [ -n "$out" ]; then + printf "%s" "$out" + echo_debug "bat" "batdrv.${_batdrv_plugin}.read_threshold($1).simulate: bm_thresh=$_bm_thresh; bf=$bf; out=$out; rc=$rc" + return 0 + fi + + if [ "$_bm_thresh" = "natacpi" ]; then + # read threshold from sysfile + case $1 in + start) bf=$_bf_start ;; + stop) bf=$_bf_stop ;; + esac + if ! out=$(read_sysf "$bf"); then + # not readable/non-existent + if [ "$2" != "1" ]; then + out="" + else + out="(not available)" + fi + rc=4 + fi + else + # no threshold api + if [ "$2" = "1" ]; then + out="(not available)" + fi + rc=255 + fi + + # "return" threshold + if [ "$X_THRESH_SIMULATE_READERR" != "1" ]; then + printf "%s" "$out" + else + if [ "$2" = "1" ]; then + printf "(not available)\n" + fi + rc=4 + fi + + echo_debug "bat" "batdrv.${_batdrv_plugin}.read_threshold($1): bm_thresh=$_bm_thresh; bf=$bf; out=$out; rc=$rc" + return $rc +} + +batdrv_write_thresholds () { + # write both charge thresholds for a battery + # use pre-determined method and sysfiles from global parms + # $1: new start threshold 0(disabled)..100/DEF(default) + # $2: new stop threshold 0..100/DEF(default) + # $3: 0=quiet/1=output parameter errors/2=output progress and errors + # $4: battery - non-empty string indicates thresholds stem from configuration + # global param: $_bm_thresh, $_bat_str, $_bf_start, $_bf_stop + # rc: 0=ok/ + # 1=not configured/ + # 2=threshold(s) out of range or non-numeric/ + # 3=minimum start stop diff violated/ + # 4=threshold read error/ + # 5=threshold write error + # prerequisite: batdrv_init(), batdrv_select_battery() + local new_start=${1:-} + local new_stop=${2:-} + local verb=${3:-0} + local cfg_bat="$4" + local old_start old_stop + + # insert defaults + [ "$new_start" = "DEF" ] && new_start=$_bt_def_start + [ "$new_stop" = "DEF" ] && new_stop=$_bt_def_stop + + # --- validate thresholds + local rc + + if [ -n "$cfg_bat" ] && [ -z "$new_start" ] && [ -z "$new_stop" ]; then + # do nothing if unconfigured + echo_debug "bat" "batdrv.${_batdrv_plugin}.write_thresholds($1, $2, $3, $4).not_configured: bat=$_bat_str" + return 1 + fi + + # TEMPLATE: customize boundaries to vendor specs + # start: check for 3 digits max, ensure min 0 / max 100 + if ! is_uint "$new_start" 3 || \ + ! is_within_bounds "$new_start" 0 100; then + # threshold out of range + echo_debug "bat" "batdrv.${_batdrv_plugin}.write_thresholds($1, $2, $3, $4).invalid_start: bat=$_bat_str" + case $verb in + 1) + if [ -n "$cfg_bat" ]; then + echo_message "Error in configuration at START_CHARGE_THRESH_${cfg_bat}=\"${new_start}\": not specified, invalid or out of range (0..100). Battery skipped." + fi + ;; + + 2) + if [ -n "$cfg_bat" ]; then + printf "Error in configuration at START_CHARGE_THRESH_%s=\"%s\": not specified, invalid or out of range (0..100). Aborted.\n" "$cfg_bat" "$new_start" 1>&2 + else + printf "Error: start charge threshold (%s) for %s is not specified, invalid or out of range (0..100). Aborted.\n" "$new_start" "$_bat_str" 1>&2 + fi + ;; + esac + return 2 + fi + + # TEMPLATE: customize boundaries to vendor specs + # stop: check for 3 digits max, ensure min 0 / max 100 + if ! is_uint "$new_stop" 3 || \ + ! is_within_bounds "$new_stop" 0 100; then + # threshold out of range + echo_debug "bat" "batdrv.${_batdrv_plugin}.write_thresholds($1, $2, $3, $4).invalid_stop: bat=$_bat_str" + case $verb in + 1) + if [ -n "$cfg_bat" ]; then + echo_message "Error in configuration at STOP_CHARGE_THRESH_${cfg_bat}=\"${new_stop}\": not specified, invalid or out of range (0..100). Battery skipped." + fi + ;; + + 2) + if [ -n "$cfg_bat" ]; then + printf "Error in configuration at STOP_CHARGE_THRESH_%s=\"%s\": not specified, invalid or out of range (0..100). Aborted.\n" "$cfg_bat" "$new_stop" 1>&2 + else + printf "Error: stop charge threshold (%s) for %s is not specified, invalid or out of range (0..100). Aborted.\n" "$new_stop" "$_bat_str" 1>&2 + fi + ;; + esac + return 2 + fi + + # TEMPLATE: customize assertion to vendor specs + # check start < stop + if [ "$new_start" -ge "$new_stop" ]; then + echo_debug "bat" "batdrv.${_batdrv_plugin}.write_thresholds($1, $2, $3, $4).invalid_diff: bat=$_bat_str" + case $verb in + 1) + if [ -n "$cfg_bat" ]; then + echo_message "Error in configuration: START_CHARGE_THRESH_${cfg_bat} >= STOP_CHARGE_THRESH_${cfg_bat}. Battery skipped." + fi + ;; + + 2) + if [ -n "$cfg_bat" ]; then + printf "Error in configuration: START_CHARGE_THRESH_%s >= STOP_CHARGE_THRESH_%s. Aborted.\n" "$cfg_bat" "$cfg_bat" 1>&2 + else + printf "Error: start threshold >= stop threshold for %s. Aborted.\n" "$_bat_str" 1>&2 + fi + ;; + esac + return 3 + fi + + # read active threshold values + if ! old_start=$(batdrv_read_threshold start 0) || \ + ! old_stop=$(batdrv_read_threshold stop 0); then + echo_debug "bat" "batdrv.${_batdrv_plugin}.write_thresholds($1, $2, $3, $4).read_error: bat=$_bat_str" + case $verb in + 1) echo_message "Error: could not read current charge threshold(s) for $_bat_str. Battery skipped." ;; + 2) printf "Error: could not read current charge threshold(s) for %s. Aborted.\n" "$_bat_str" 1>&2 ;; + esac + return 4 + fi + + # TEMPLATE: customize assertion to vendor specs + # determine write sequence too meet boundary condition start < stop + # disclaimer: the driver doesn't enforce it but we don't know about the + # firmware and it's reasonable anyway + local rc=0 steprc tseq + + if [ "$new_start" -ge "$old_stop" ]; then + tseq="stop start" + else + tseq="start stop" + fi + + # write new thresholds in determined sequence + if [ "$verb" = "2" ]; then + printf "Setting temporary charge thresholds for %s:\n" "$_bat_str" + fi + + for step in $tseq; do + local old_thresh new_thresh steprc + + case $step in + start) + old_thresh=$old_start + new_thresh=$new_start + ;; + + stop) + old_thresh=$old_stop + new_thresh=$new_stop + ;; + esac + + if [ "$old_thresh" != "$new_thresh" ]; then + # new threshold differs from effective one --> write it + case $step in + start) write_sysf "$new_thresh" "$_bf_start" ;; + stop) write_sysf "$new_thresh" "$_bf_stop" ;; + esac + steprc=$?; [ $steprc -ne 0 ] && rc=5 + echo_debug "bat" "batdrv.${_batdrv_plugin}.write_thresholds($1, $2, $3, $4).$step.write: bat=$_bat_str; old=$old_thresh; new=$new_thresh; steprc=$steprc" + + case $verb in + 2) + if [ $steprc -eq 0 ]; then + printf " %-5s = %3d\n" "$step" "$new_thresh" + else + printf " %-5s = %3d (Error: write failed)\n" "$step" "$new_thresh" 1>&2 + fi + ;; + 1) + if [ $steprc -gt 0 ]; then + echo_message "Error: writing charge $step threshold for $_bat_str failed." + fi + ;; + esac + else + echo_debug "bat" "batdrv.${_batdrv_plugin}.write_thresholds($1, $2, $3, $4).$step.no_change: bat=$_bat_str; old=$old_thresh; new=$new_thresh" + + if [ "$verb" = "2" ]; then + printf " %-5s = %3d (no change)\n" "$step" "$new_thresh" + fi + fi + done # for step + + echo_debug "bat" "batdrv.${_batdrv_plugin}.write_thresholds($1, $2, $3, $4).complete: bat=$_bat_str; rc=$rc" + return $rc +} + +batdrv_chargeonce () { + # function not implemented + 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 + # output parameter errors only + + if batdrv_select_battery BAT0; then + batdrv_write_thresholds "$START_CHARGE_THRESH_BAT0" "$STOP_CHARGE_THRESH_BAT0" 1 "BAT0"; rc=$? + fi + if batdrv_select_battery BAT1; then + batdrv_write_thresholds "$START_CHARGE_THRESH_BAT1" "$STOP_CHARGE_THRESH_BAT1" 1 "BAT1"; rc=$? + fi + + return 0 +} + +batdrv_read_force_discharge () { + # function not implemented + echo_debug "bat" "batdrv.${_batdrv_plugin}.read_force_discharge.not_implemented" + return 255 +} + +batdrv_write_force_discharge () { + # function not implemented + echo_debug "bat" "batdrv.${_batdrv_plugin}.write_force_discharge.not_implemented" + return 255 +} + +batdrv_cancel_force_discharge () { + # function not implemented + echo_debug "bat" "batdrv.${_batdrv_plugin}.cancel_force_discharge.not_implemented" + return 255 +} + +batdrv_force_discharge_active () { + # function not implemented + echo_debug "bat" "batdrv.${_batdrv_plugin}.force_discharge_active.not_implemented" + return 255 +} + +batdrv_discharge () { + # function not implemented + + # 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 param: $_batteries + # prerequisite: batdrv_init() + local verbose=${1:-0} + + printf "+++ Battery Care\n" + printf "Plugin: %s\n" "$_batdrv_plugin" + + if [ "$_bm_thresh" != "none" ]; then + printf "Supported features: charge thresholds\n" + else + printf "Supported features: none available\n" + fi + + printf "Driver usage:\n" + # native kernel ACPI battery API + case $_natacpi in + 0) printf "* natacpi (%s) = active (charge thresholds)\n" "$_batdrv_kmod" ;; + 32) printf "* natacpi (%s) = inactive (disabled by configuration)\n" "$_batdrv_kmod" ;; + 128) printf "* natacpi (%s) = inactive (no kernel support)\n" "$_batdrv_kmod" ;; + 254) printf "* natacpi (%s) = inactive (laptop not supported)\n" "$_batdrv_kmod" ;; + *) printf "* natacpi (%s) = unknown status\n" "$_batdrv_kmod" ;; + esac + # TEMPLATE: customize to vendor specs + if [ "$_bm_thresh" != "none" ]; then + printf "Parameter value ranges:\n" + printf "* START_CHARGE_THRESH_BAT0/1: 0(off)..100\n" + printf "* STOP_CHARGE_THRESH_BAT0/1: 0..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" + printparm "%-59s = ##%s##" "$_bd_read/serial_number" + + 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_start" "$(batdrv_read_threshold start)" + printf "%-59s = %6s [%%]\n" "$_bf_stop" "$(batdrv_read_threshold stop)" + 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 +}