LoginSignup
3
2

More than 5 years have passed since last update.

WSLでEmacsのbattery.elを使えるようにする

Last updated at Posted at 2018-12-31

背景

paypay20%キャンペーンと18,000円キャシュバックに惹かれて
Surface pro 6 を購入し、Emacs環境をWSLで新たに構築しました
その際に、タイトルバーに表示していたバッテリー残量が出ていませんでした
携帯機なのでバッテリー表示は必須と思い対策を調査しました
emacs-battery.png

結論

下記コードで解決

battery-wsl.el
;; battery-modeの設定
(setq battery-status-function #'battery-linux-sysfs-wsl) ; バッテリー情報取得関数を独自のものに置き換える
;;(setq battery-update-interval 60) ; 必要があれば更新頻度設定[sec]
(display-battery-mode 1)
(setq battery-mode-line-format " %b%p%%")

;; タイトルバーに時計とバッテリー情報表示(こちらを使用するのであれば上記のbattery-modeの設定は不要)
(when (window-system)
  ;; display-timeより先にsetしておかないとdefaultの書式になる
  (setq display-time-string-forms
    '((format "%s/%s/%s" year month day)
      (format "(%s:%s)" 24-hours minutes)))
  ;;(setq display-time-interval 1)
  (display-time) ;; display-time-stringの有効化
  ;; タイトルバーの書式設定 global-mode-stringにdisplay-time-stringが入っている
  ;; バッファがファイルのときはフルパス、でなければバッファ名表示
  ;; if(buffer-file-name) の評価がsetq時で終わらないよう:eval
  (setq battery-status-function #'battery-linux-sysfs-wsl)
  ;;(setq battery-update-interval 1)
  (display-battery-mode 1)
  (setq battery-mode-line-format " %b%p%%")
  (setq frame-title-format '("" (:eval (if (buffer-file-name) " %f" " %b"))
                 " --- " global-mode-string) ) )

;; バッテリー取得関数本体(でかいのでinit-loaderで分割をオススメ)
(defun battery-linux-sysfs-wsl ()
  "Get ACPI status information from Linux kernel.
This function works only with the Windows Subsystem for Linux.

The following %-sequences are provided:
%c Current capacity (mAh)
%r Current rate
%B Battery status (verbose)
%b Battery status (charging:'+' discharging:'')
%d Temperature (in degrees Celsius)
%p Battery load percentage
%L AC line status (verbose)
%m Remaining time (to charge or discharge) in minutes
%h Remaining time (to charge or discharge) in hours
%t Remaining time (to charge or discharge) in the form `h:min'"
  (let (charging-state ac-state temperature hours
               energy-now energy-now-rate power-now current-now voltage-now
               (dir "/sys/class/power_supply/battery"))
    (with-temp-buffer
      (erase-buffer)
      (ignore-errors (insert-file-contents
              (expand-file-name "capacity" dir)))
      (setq energy-now-rate (or (thing-at-point 'number) "N/A"))

      (erase-buffer)
      (ignore-errors (insert-file-contents
              (expand-file-name "status" dir)))
      (setq charging-state (or (thing-at-point 'word) "N/A"))

      (erase-buffer)
      (ignore-errors (insert-file-contents
              (expand-file-name "temp" dir)))
      (setq temperature (or (thing-at-point 'number) "N/A"))
      (setq temperature (if (numberp temperature) (* temperature 0.1)))

      (erase-buffer)
      (ignore-errors (insert-file-contents
              (expand-file-name "charge_counter" dir)))
      (setq energy-now (or (thing-at-point 'number) "N/A"))

      (erase-buffer)
      (ignore-errors (insert-file-contents
              (expand-file-name "current_now" dir)))
      (setq current-now (or (thing-at-point 'number) "N/A"))
      (unless (or   (stringp energy-now) (stringp current-now)
            (stringp energy-now-rate) (zerop current-now))
    (if (string= charging-state "Discharging")
        (setq hours (/ energy-now current-now))
      (setq hours (/ (* energy-now (- 100.0 energy-now-rate))
             energy-now-rate current-now ))))

      (erase-buffer)
      (ignore-errors (insert-file-contents
              (expand-file-name "voltage_now" dir)))
      (setq voltage-now (or (thing-at-point 'number) "N/A"))
      (setq power-now (if (and (numberp current-now) (numberp voltage-now))
              (* (/ current-now 1000.0) (/ voltage-now 1000000.0))))
      ;; current-now[mA]->[A] voltage-now[uV]->[V]

      (erase-buffer)
      (setq dir "/sys/class/power_supply/ac")
      (ignore-errors (insert-file-contents
              (expand-file-name "online" dir)))
      (setq ac-state (cond ((eq (thing-at-point 'number) 1) "AC")
               ((eq (thing-at-point 'number) 0) "BAT")
               (t "N/A")))
      )
    ;; set return value
    (list (cons ?c (number-to-string energy-now))
      (cons ?r (if hours (number-to-string power-now) "N/A"))
      (cons ?B charging-state)
      (cons ?b (if (string= charging-state "Charging") "+" ""))
      (cons ?d (number-to-string temperature))
      (cons ?p (number-to-string energy-now-rate))
      (cons ?L ac-state)
      (cons ?m (if hours (format "%d" (* hours 60)) "N/A"))
      (cons ?h (if hours (format "%d" hours) "N/A"))
      (cons ?t (if hours (format "%d:%02d" hours (* (- hours (floor hours)) 60)) "N/A")))
    )
)

一応、関数本体だけの.elをこちら#githubに用意しました

調査ログ

バッテリー情報取得関数を探す

モードラインへの表示を制御するのに以下ように設定していました
(setq battery-mode-line-format " %b%p%%")
このbattery-mode-line-formatC-h vで変数の*Help*を見てみると

*Help*
battery-mode-line-format is a variable defined in battery.el.
Its value is " %b%p%%"
Original value was nil

Documentation:
Control string formatting the string to display in the mode line.
Ordinary characters in the control string are printed as-is, while
conversion specifications introduced by a % character in the control
string are substituted as defined by the current value of the variable
battery-status-function.  Here are the ones generally available:
%c Current capacity (mAh or mWh)
%r Current rate of charge or discharge
%B Battery status (verbose)
%b Battery status: empty means high, - means low,
   ! means critical, and + means charging
%d Temperature (in degrees Celsius)
%L AC line status (verbose)
%p Battery load percentage
%m Remaining time (to charge or discharge) in minutes
%h Remaining time (to charge or discharge) in hours
%t Remaining time (to charge or discharge) in the form h:min

You can customize this variable.

battery-status-function変数の値によって%*が差し替えられるとあります

というわけで、battery-status-function変数に入っている関数が
バッテリー情報を取得していると推測されます

現在の値は*Help*内のbattery-status-functionのリンクをRETすると

battery-status-function is a variable defined in ‘battery.el’.
Its value is nil

  This variable may be risky if used as a file-local variable.

Documentation:
Function for getting battery status information.
The function has to return an alist of conversion definitions.
Its cons cells are of the form

    (CONVERSION . REPLACEMENT-TEXT)

CONVERSION is the character code of a "conversion specification"
introduced by a ‘%’ character in a control string.

You can customize this variable.

nilとなっております
*Help*内のbattery.elのリンクからコードへジャンプすると

battery.el
(defcustom battery-status-function
  (cond ((and (eq system-type 'gnu/linux)
          (file-readable-p "/proc/apm"))
     #'battery-linux-proc-apm)
    ((and (eq system-type 'gnu/linux)
          (file-directory-p "/proc/acpi/battery"))
     #'battery-linux-proc-acpi)
    ((and (eq system-type 'gnu/linux)
          (file-directory-p "/sys/class/power_supply/")
          (directory-files "/sys/class/power_supply/" nil
                               battery--linux-sysfs-regexp))
     #'battery-linux-sysfs)
    ((and (eq system-type 'berkeley-unix)
          (file-executable-p "/usr/sbin/apm"))
     #'battery-bsd-apm)
    ((and (eq system-type 'darwin)
          (condition-case nil
          (with-temp-buffer
            (and (eq (call-process "pmset" nil t nil "-g" "ps") 0)
             (> (buffer-size) 0)))
        (error nil)))
     #'battery-pmset)
    ((fboundp 'w32-battery-status)
     #'w32-battery-status))
  "Function for getting battery status information.
The function has to return an alist of conversion definitions.
Its cons cells are of the form

    (CONVERSION . REPLACEMENT-TEXT)

CONVERSION is the character code of a \"conversion specification\"
introduced by a `%' character in a control string."
  :type '(choice (const nil) function)
  :group 'battery)

とあり、環境によって

  • battery-linux-proc-apm
  • battery-linux-proc-acpi
  • battery-linux-sysfs
  • battery-bsd-apm
  • battery-pmset
  • w32-battery-status

がセットされるようです

このなかでWSL環境に近いのは、Pathが"/sys/class/power_supply/"でセットされる
battery-linux-sysfsなのでこの関数が目的の関数となります

バッテリー情報が取れない原因

WSLでは一般のlinux環境とは異なるpathでバッテリー情報を提供しているためでした
battery-linux-sysfsでは"/sys/class/power_supply/bat0/"を期待しているのですが
WSL環境では、"/sys/class/power_supply/battery/"となっており、その配下のファイル構成も異なっております

shell.log
$ ls /sys/class/power_supply/
ac  battery  usb
$ ls /sys/class/power_supply/battery/
capacity  charge_counter  current_now  health  present  status  technology  temp  type  voltage_now
$ cat /sys/class/power_supply/battery/capacity
64 # 残量[%]
$ cat /sys/class/power_supply/battery/charge_counter 
3959 # 充電容量[mAh]
$ cat /sys/class/power_supply/battery/current_now 
705 # 電流[mA]
$ cat /sys/class/power_supply/battery/health 
Good # 劣化状態
$ cat /sys/class/power_supply/battery/present 
1 # 電力供給状態?
$ cat /sys/class/power_supply/battery/status 
Discharging # 充電状態(非充電中)
$ cat /sys/class/power_supply/battery/status 
Charging # 充電状態(充電中)
$ cat /sys/class/power_supply/battery/status 
Full # 充電状態(充電完了)
$ cat /sys/class/power_supply/battery/technology 
Li-ion # 製品仕様
$ cat /sys/class/power_supply/battery/temp
258 # 温度 摂氏x10 ?
$ cat /sys/class/power_supply/battery/type
Battery # 電力供給仕様
$ cat /sys/class/power_supply/battery/voltage_now 
8115000 # 電圧 [uV] => 8.115[V] ?

WSL用のバッテリー情報取得関数の設計

バッテリー情報取得関数が返す必要がある情報は
battery-linux-sysfsを参考にすると

*Help*
battery-linux-sysfs is a compiled Lisp function in battery.el.

(battery-linux-sysfs)

Get ACPI status information from Linux kernel.
This function works only with the new /sys/class/power_supply/
format introduced in Linux version 2.4.25.

The following %-sequences are provided:
%c Current capacity (mAh or mWh)
%r Current rate
%B Battery status (verbose)
%d Temperature (in degrees Celsius)
%p Battery load percentage
%L AC line status (verbose)
%m Remaining time (to charge or discharge) in minutes
%h Remaining time (to charge or discharge) in hours
%t Remaining time (to charge or discharge) in the form h:min

%c Current capacity (mAh or mWh)

charge_counterの値を返したら良さそう

%r Current rate

current_nowvoltate_nowの積で電力[w]を計算したらいけそう

%B Battery status (verbose)

battery/statusの値を返したら良さそう

%d Temperature (in degrees Celsius)

tempの値を返したら良さそう

%p Battery load percentage

capacityの値を返したら良さそう

%L AC line status (verbose)

ac/statusの値を返したら良さそう

%m Remaining time (to charge or discharge) in minutes

%h Remaining time (to charge or discharge) in hours

%t Remaining time (to charge or discharge) in the form `h:min'"

2つのアルゴリズムが必要

残り稼働時間(非充電時)

残り稼働時間[h] = 電池残容量[Wh] / 消費電力[Wh] = 電池容量[Ah] / 消費電力[Ah]
電池容量[Ah] = charge_counter[mAh] * 1000
消費電力[Ah] = current_now[mA] * 1000 * 1[h]

残り充電時間(充電時)

残り充電時間[h] = (電池満容量[Wh] - 電池残容量[Wh]) / 供給電力[Wh]
電池満容量[Wh]が情報としてないので
電池満容量[Wh] * 電池残量[%] = 電池残容量[Wh]
電池満容量[Wh] = 電池残容量[Wh] / 電池残量[%]

(電池満容量[Wh] - 電池残容量[Wh]) = 電池残容量[Wh] / 電池残量[%] - 電池残量[%]
= 電池残容量[Wh] * (1 - 電池残量[%]) / 電池残量[%]
= V[V] * charge_counter[mAh] * 1000 * (1 - capacity) / capacity
残り充電時間[h] = V[V] * charge_counter[mAh] * 1000 * (1 - capacity) / capacity / (V[V] * current_now[mA] * 1000 * 1[h])
= charge_counter[mAh] * (1 - capacity) / capacity / current_now[mA]

WSL用のバッテリー情報取得関数の実装

前章で述べた情報を
(list (cons ?c val) (cons ?r val) ...)
の形で返す関数を実装します
lispなので最後に評価されたS式が戻り値となります

battery-wsl.el
(let (charging-state ac-state temperature hours
             energy-now energy-now-rate power-now current-now voltage-now
             (dir "/sys/class/power_supply/battery"))
  (with-temp-buffer ;; このブロック限りのバッファを生成しcurrent_bufferとする
    (erase-buffer) ;; バッファに読み込む前にバッファを空に
        (ignore-errors (insert-file-contents
            (expand-file-name "capacity" dir)))
    (setq energy-now-rate (or (thing-at-point 'number) "N/A"))

    (erase-buffer)
    (ignore-errors (insert-file-contents
            (expand-file-name "status" dir)))
    (setq charging-state (or (thing-at-point 'word) "N/A"))

    (erase-buffer)
    (ignore-errors (insert-file-contents
            (expand-file-name "temp" dir)))
    (setq temperature (or (thing-at-point 'number) "N/A"))
    (setq temperature (if (numberp temperature) (* temperature 0.1)))

    (erase-buffer)
    (ignore-errors (insert-file-contents
            (expand-file-name "charge_counter" dir)))
    (setq energy-now (or (thing-at-point 'number) "N/A"))

    (erase-buffer)
    (ignore-errors (insert-file-contents
            (expand-file-name "current_now" dir)))
    (setq current-now (or (thing-at-point 'number) "N/A"))
    (unless (or (stringp energy-now) (stringp current-now)
        (stringp energy-now-rate) (zerop current-now))
      ;; 変数がN/Aのときパス。充電完了となるとcurrent-now=0。zero-dev回避のため0のときパス
      (if (string= charging-state "Discharging")
          (setq hours (/ energy-now current-now))
        (setq hours (/ (* energy-now (- 100.0 energy-now-rate))
                   energy-now-rate current-now ))))

    (erase-buffer)
    (ignore-errors (insert-file-contents
            (expand-file-name "voltage_now" dir)))
    (setq voltage-now (or (thing-at-point 'number) "N/A"))
    (setq power-now (if (and (numberp current-now) (numberp voltage-now))
            (* (/ current-now 1000.0) (/ voltage-now 1000000.0))))
    ;; current-now[mA]->[A] voltage-now[uV]->[V]

    (erase-buffer)
    (setq dir "/sys/class/power_supply/ac") ;; acはpathが変わるので変更
    (ignore-errors (insert-file-contents
            (expand-file-name "online" dir)))
    (setq ac-state (cond ((eq (thing-at-point 'number) 1) "AC")
             ((eq (thing-at-point 'number) 0) "BAT")
             (t "N/A")))
    )
  ;; 戻り値(リスト型)を設定
  (list (cons ?c energy-now)
    (cons ?r (if hours power-now "N/A"))
    (cons ?B charging-state)
    (cons ?d temperature)
    (cons ?p energy-now-rate)
    (cons ?L ac-state)
    (cons ?m (if hours (format "%d" (* hours 60)) "N/A"))
    (cons ?h (if hours (format "%d" hours) "N/A"))
    (cons ?t (if hours (format "%d:%02d" hours (* (- hours (floor hours)) 60)) "N/A")))
  )

こいつをdefunで囲えば完成です

最後に

今回は、実装までの過程を詳細に書いてみました
Emacsにはeldoc*Help*が用意されている上に
ソースコードにも直接アクセスできるのですが
その活かし方がイマイチ広まってないと思います
*Help*が使いこなせるとEmacsの拡張がもっと楽しめるので
皆さん使って見ましょう

  • C-h v 変数(variable)のhelp
  • C-h f 関数(function)のhelp
  • C-h b キーマップ(bind)のhelp
3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2