commit 463c1eea030921cb77c93dda680e43268fe81ca1
parent b6a8048c8bb2e9fdb46dd688173ac1c71f3924a0
Author: Daniel Barlow <dan@telent.net>
Date: Thu, 7 Apr 2022 10:00:49 +0100
standardise interface to metrics
call as follows:
(let [m (metricname.new)]
(m:read)
(m:read)
...
)
Diffstat:
9 files changed, 218 insertions(+), 209 deletions(-)
diff --git a/blinkenlicht/README.md b/blinkenlicht/README.md
@@ -17,9 +17,9 @@ Not quite dogfood-ready yet, but fast approaching.
* blinkenlicht.fnl is the module that parses `bar` and `indicator`
forms and does all the UI
-* metric.fnl is a collection of functions that read metrics (load
- average, battery status etc) from the system, for indicators to
- display.
+* metric/*.fnl is a collection of modules that read metrics (load
+ average, connectivity, battery status etc) from the system, for
+ indicators to display.
Use the `default.nix` for guidance as to libraries and other setup
required - or just use it, of course.
@@ -43,10 +43,10 @@ Gtk will find it anyway. Magic.
* [X] add some mechanism for indicators that wait for events instead of polling
* [X] set indicator background colour (use css for this?)
* [ ] update image/label widget instead of destroying
-* [ ] allow height customisation
+* [ ] allow image height customisation
* [X] set poll interval based on indicators' requested intervals
* [X] allow indicator to change styles based on status
* [ ] add metrics for
- - wireless connected/strength
+ - [X] wireless connected/strength
- wwan connected/type (hspa, lte, etc)/signal
- cpu %
diff --git a/blinkenlicht/bl.fnl b/blinkenlicht/bl.fnl
@@ -3,10 +3,12 @@
(local {: view} (require :fennel))
(local iostream (require :iostream))
-(local metric (require :metric))
-(local uplink (require :uplink))
(local modem (require :modem))
+(local uplink (require :metric.uplink))
+(local battery (require :metric.battery))
+(local cpustat (require :metric.cpustat))
+
(stylesheet "licht.css")
(fn battery-icon-codepoint [status percent]
@@ -40,26 +42,27 @@
(indicator {
:wait-for { :interval (* 1 500) }
:refresh
- #(let [current (. (metric.cpustat) :iowait)
- delta (- current previous)
- v (if (> delta 4) "" " ")]
- (set previous current)
- {:text v})
+ (let [stat (cpustat.new)]
+ #(let [current (. (stat:read) :iowait)
+ delta (- current previous)
+ v (if (> delta 4) "" " ")]
+ (set previous current)
+ {:text v}))
}))
- (let [modem (modem.new)]
- (indicator {
- :wait-for {
+ (indicator {
+ :wait-for {
:interval (* 4 1000)
- }
- :refresh
+ }
+ :refresh
+ (let [modem (modem.new)]
#(let [{:m3gpp-operator-name operator
- :signal-quality quality} (modem:value)]
+ :signal-quality quality} (modem:read)]
{:text (.. operator
- ;" " (. quality 1) "dBm"
+ ;;" " (. quality 1) "dBm"
)
- })
- }))
+ }))
+ })
(let [uplink (uplink.new)
input (iostream.from-descriptor uplink.fd)]
@@ -68,7 +71,7 @@
:input [input]
}
:refresh
- #(let [status (uplink:status)]
+ #(let [status (uplink:read)]
(if status
{:text (.. (or status.ssid status.name "?")
" "
@@ -85,15 +88,17 @@
(indicator {
:wait-for { :interval (* 1000 10) }
:refresh
- #(let [{:power-supply-capacity percent
- :power-supply-status status}
- (metric.battery "axp20x-battery")
- icon-code (battery-icon-codepoint
- status (tonumber percent))]
- {:text
- (string.format "%s %d%%" (utf8.char icon-code) percent)
- :classes ["battery" (if (< (tonumber percent) 20) "low" "ok")]
- })
+ (let [battery (battery.new (or (os.getenv "BLINKEN_BATTERY")
+ "axp20x-battery"))]
+ #(let [{:power-supply-capacity percent
+ :power-supply-status status}
+ (battery.read)
+ icon-code (battery-icon-codepoint
+ status (tonumber percent))]
+ {:text
+ (string.format "%s %d%%" (utf8.char icon-code) percent)
+ :classes ["battery" (if (< (tonumber percent) 20) "low" "ok")]
+ }))
})
(indicator {
:wait-for { :interval 4000 }
diff --git a/blinkenlicht/metric.fnl b/blinkenlicht/metric.fnl
@@ -1,42 +0,0 @@
-(local {: view} (require :fennel))
-
-(fn loadavg []
- (with-open [f (io.open "/proc/loadavg" :r)]
- (let [line (f:read "*a")
- (one five fifteen) (line:match "([%d.]+) +([%d.]+) +([%d.]+)")]
- (values (tonumber one) (tonumber five) (tonumber fifteen)))))
-
-(fn battery [name]
- (let [name (.. "/sys/class/power_supply/" name "/uevent")]
- (with-open [f (io.open name :r)]
- (let [fields {}]
- (each [line #(f:read "*l")]
- (let [(name value) (line:match "([^=]+)=(.+)")]
- (tset fields (: (name:gsub "_" "-") :lower) value)))
- fields))))
-
-(fn parse-cpu-stat-line [line]
- (let [labels [:user :nice :system :idle :iowait
- :irq :softirq :steal :guest :guest_nice]
- vals (icollect [field (line:gmatch "([%d.]+)")]
- (tonumber field))]
- (collect [i label (ipairs labels)]
- label (. vals i))))
-
-(var proc-stat-handle nil)
-
-(fn cpustat [path]
- (if proc-stat-handle
- (proc-stat-handle:seek :set 0)
- (set proc-stat-handle (io.open "/proc/stat" :r)))
- (let [f proc-stat-handle]
- (accumulate [ret nil
- line #(f:read "*l") ]
- (if (= (string.sub line 1 (# "cpu ")) "cpu ")
- (parse-cpu-stat-line line)
- ret))))
-
-{: loadavg
- : battery
- : cpustat
- }
diff --git a/blinkenlicht/metric/battery.fnl b/blinkenlicht/metric/battery.fnl
@@ -0,0 +1,15 @@
+(fn battery [name]
+ (let [name (.. "/sys/class/power_supply/" name "/uevent")]
+ (with-open [f (io.open name :r)]
+ (let [fields {}]
+ (each [line #(f:read "*l")]
+ (let [(name value) (line:match "([^=]+)=(.+)")]
+ (tset fields (: (name:gsub "_" "-") :lower) value)))
+ fields))))
+
+{ :new
+ (fn [name]
+ {
+ :read #(battery name)
+ })
+ }
diff --git a/blinkenlicht/metric/cpustat.fnl b/blinkenlicht/metric/cpustat.fnl
@@ -0,0 +1,24 @@
+(fn parse-cpu-stat-line [line]
+ (let [labels [:user :nice :system :idle :iowait
+ :irq :softirq :steal :guest :guest_nice]
+ vals (icollect [field (line:gmatch "([%d.]+)")]
+ (tonumber field))]
+ (collect [i label (ipairs labels)]
+ label (. vals i))))
+
+(fn cpustat [proc-stat-handle]
+ (let [f proc-stat-handle]
+ (f:seek :set 0)
+ (accumulate [ret nil
+ line #(f:read "*l")
+ :until ret]
+ (if (= (string.sub line 1 (# "cpu ")) "cpu ")
+ (parse-cpu-stat-line line)
+ ret))))
+{
+ :new
+ #(let [handle (io.open "/proc/stat" :r)]
+ {
+ :read #(cpustat handle)
+ })
+ }
diff --git a/blinkenlicht/metric/loadavg.fnl b/blinkenlicht/metric/loadavg.fnl
@@ -0,0 +1,9 @@
+(fn loadavg []
+ (with-open [f (io.open "/proc/loadavg" :r)]
+ (let [line (f:read "*a")
+ (one five fifteen) (line:match "([%d.]+) +([%d.]+) +([%d.]+)")]
+ (values (tonumber one) (tonumber five) (tonumber fifteen)))))
+
+{:new #{
+ :read #(loadavg)
+ })
diff --git a/blinkenlicht/metric/uplink.fnl b/blinkenlicht/metric/uplink.fnl
@@ -0,0 +1,119 @@
+(local nl (require :netlink))
+(local view (. (require :fennel) :view))
+
+;; $ grep DEVTYPE /sys/class/net/*/uevent
+;; /sys/class/net/docker0/uevent:DEVTYPE=bridge
+;; /sys/class/net/wlp4s0/uevent:DEVTYPE=wlan
+;; /sys/class/net/wwp0s20f0u2i12/uevent:DEVTYPE=wwan
+;; (ethernet and loopback devices don't have DEVTYPE)
+
+(fn devtype [ifname]
+ (with-open [f (io.open (.. "/sys/class/net/" ifname "/uevent") :r)]
+ (accumulate [dtype nil
+ line #(f:read "*l")
+ :until dtype]
+ (let [(name value) (line:match "([^=]+)=(.+)")]
+ (if (= name "DEVTYPE") value dtype)))))
+
+;; if the type is wlan, we can get a signal strength indicator
+;; from the "quality - link" column of /proc/net/wireless
+
+(fn wlan-link-quality [ifname]
+ (with-open [f (io.open "/proc/net/wireless" :r)]
+ (accumulate [strength nil
+ line #(f:read "*l")
+ :until strength]
+ ;; "%6s: %04x %3d%c %3d%c %3d%c %6d %6d %6d "
+ ;; "%6d %6d %6d\n",
+ ;; https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/net/wireless/wext-proc.c#n49
+
+ (let [(name status link level)
+ (line:match "(.-): +(%x+) +(%S-)[ .] +(%S-)[ .]")]
+ (if (= name ifname)
+ (tonumber level)
+ strength)))))
+
+(fn wlan-link-ssid [ifname]
+ ;; could do this directly using an ioctl
+ ;; http://papermint-designs.com/dmo-blog/2016-08-how-to-get-the-essid-of-the-wifi-network-you-are-connected-to-#
+ (with-open [f (io.popen (.. "iwgetid " ifname " --raw") :r)]
+ (f:read "*l")))
+
+(fn get-network-info [event]
+ ;; augments a newlink event with relevant information, if it's
+ ;; sufficiently well-configured to have any
+
+ ;; "up" => administratively up
+ ;; "running" => can actually exchange packets
+ (when (= event.running "yes")
+ (let [dtype (devtype event.name)]
+ (tset event :devtype dtype)
+ (when (= dtype "wlan")
+ (if (not event.ssid)
+ (tset event :ssid (wlan-link-ssid event.name)))
+ (tset event :quality (wlan-link-quality event.name)))
+ (when (= dtype "wwan")
+ ;; for wwan, need to determine how to get strength and carrier name
+ true)
+ ))
+ event)
+
+(fn uplink []
+ (let [links {}
+ routes {}
+ sock (nl.socket)]
+ (fn handle-event [event]
+ (match event
+ {:event :newlink}
+ (tset links event.index
+ (match event.up
+ "yes" (get-network-info event)
+ "no" event))
+
+ {:event :newroute}
+ ;; XXX there may be >1 route to any given destination,
+ ;; (e.g wwan and wlan both have default route)
+ ;; we probably need to store all of them and
+ ;; distinguish by metric
+ (let [dst (or event.dst "default")
+ existing (. routes dst)]
+ (if (or (not existing)
+ (< event.metric existing.metric))
+ (tset routes dst event)))
+
+ {} (print :unhandled event.event)
+ ))
+ (each [_ event (ipairs (sock:query ))]
+ (handle-event event))
+
+ {
+ :refresh #(each [_ event (ipairs (sock:event))]
+ (handle-event event))
+ :fd (sock:fd)
+ :read (fn [self]
+ (self:refresh)
+ (let [defaultroute routes.default
+ interface (and defaultroute
+ (. links defaultroute.index))]
+ (and interface (= interface.running "yes")
+ (get-network-info interface))))
+ :wait #(sock:poll 1000)
+ :interface (fn [self ifnum]
+ (. links ifnum))
+ }
+ ))
+
+(comment
+(let [nl (netlunk)]
+ (while (or (nl:wait) true)
+ (nl:refresh)
+ (match (nl:uplink)
+ interface
+ (print "default route through " (view interface))
+ nil
+ (print "no default route")
+ ))))
+
+{
+ :new uplink
+ }
diff --git a/blinkenlicht/modem.fnl b/blinkenlicht/modem.fnl
@@ -5,25 +5,22 @@
(local variant dbus.variant)
;; https://www.freedesktop.org/software/ModemManager/api/latest/ref-dbus.html
-(var the-modem-manager nil)
+
(fn modem-manager []
- (when (not the-modem-manager)
- (set the-modem-manager
- (dbus.Proxy:new
- {
- :bus dbus.Bus.SYSTEM
- :name "org.freedesktop.ModemManager1"
- :interface "org.freedesktop.DBus.ObjectManager"
- :path "/org/freedesktop/ModemManager1"
- })))
- the-modem-manager)
+ (dbus.Proxy:new
+ {
+ :bus dbus.Bus.SYSTEM
+ :name "org.freedesktop.ModemManager1"
+ :interface "org.freedesktop.DBus.ObjectManager"
+ :path "/org/freedesktop/ModemManager1"
+ }))
;; this is a function because the path to the modem may change
;; (e.g. due to suspend/resume cycles causing services to be stopped
;; and started)
-(fn modem-interface []
- (let [modem-path (next (: (assert (modem-manager)) :GetManagedObjects))]
+(fn modem-interface [manager]
+ (let [modem-path (next (: (assert manager) :GetManagedObjects))]
(dbus.Proxy:new
{
:bus dbus.Bus.SYSTEM
@@ -33,10 +30,11 @@
})))
(fn new-modem-status []
- {
- :value #(let [m (modem-interface)]
- (variant.strip (m:GetStatus)))
- })
+ (let [manager (modem-manager)]
+ {
+ :read #(let [m (modem-interface manager)]
+ (variant.strip (m:GetStatus)))
+ }))
(comment
(let [ctx (: (GLib.MainLoop) :get_context)
diff --git a/blinkenlicht/uplink.fnl b/blinkenlicht/uplink.fnl
@@ -1,119 +0,0 @@
-(local nl (require :netlink))
-(local view (. (require :fennel) :view))
-
-;; $ grep DEVTYPE /sys/class/net/*/uevent
-;; /sys/class/net/docker0/uevent:DEVTYPE=bridge
-;; /sys/class/net/wlp4s0/uevent:DEVTYPE=wlan
-;; /sys/class/net/wwp0s20f0u2i12/uevent:DEVTYPE=wwan
-;; (ethernet and loopback devices don't have DEVTYPE)
-
-(fn devtype [ifname]
- (with-open [f (io.open (.. "/sys/class/net/" ifname "/uevent") :r)]
- (accumulate [dtype nil
- line #(f:read "*l")
- :until dtype]
- (let [(name value) (line:match "([^=]+)=(.+)")]
- (if (= name "DEVTYPE") value dtype)))))
-
-;; if the type is wlan, we can get a signal strength indicator
-;; from the "quality - link" column of /proc/net/wireless
-
-(fn wlan-link-quality [ifname]
- (with-open [f (io.open "/proc/net/wireless" :r)]
- (accumulate [strength nil
- line #(f:read "*l")
- :until strength]
- ;; "%6s: %04x %3d%c %3d%c %3d%c %6d %6d %6d "
- ;; "%6d %6d %6d\n",
- ;; https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/net/wireless/wext-proc.c#n49
-
- (let [(name status link level)
- (line:match "(.-): +(%x+) +(%S-)[ .] +(%S-)[ .]")]
- (if (= name ifname)
- (tonumber level)
- strength)))))
-
-(fn wlan-link-ssid [ifname]
- ;; could do this directly using an ioctl
- ;; http://papermint-designs.com/dmo-blog/2016-08-how-to-get-the-essid-of-the-wifi-network-you-are-connected-to-#
- (with-open [f (io.popen (.. "iwgetid " ifname " --raw") :r)]
- (f:read "*l")))
-
-(fn get-network-info [event]
- ;; augments a newlink event with relevant information, if it's
- ;; sufficiently well-configured to have any
-
- ;; "up" => administratively up
- ;; "running" => can actually exchange packets
- (when (= event.running "yes")
- (let [dtype (devtype event.name)]
- (tset event :devtype dtype)
- (when (= dtype "wlan")
- (if (not event.ssid)
- (tset event :ssid (wlan-link-ssid event.name)))
- (tset event :quality (wlan-link-quality event.name)))
- (when (= dtype "wwan")
- ;; for wwan, need to determine how to get strength and carrier name
- true)
- ))
- event)
-
-(fn uplink []
- (let [links {}
- routes {}
- sock (nl.socket)]
- (fn handle-event [event]
- (match event
- {:event :newlink}
- (tset links event.index
- (match event.up
- "yes" (get-network-info event)
- "no" event))
-
- {:event :newroute}
- ;; XXX there may be >1 route to any given destination,
- ;; (e.g wwan and wlan both have default route)
- ;; we probably need to store all of them and
- ;; distinguish by metric
- (let [dst (or event.dst "default")
- existing (. routes dst)]
- (if (or (not existing)
- (< event.metric existing.metric))
- (tset routes dst event)))
-
- {} (print :unhandled event.event)
- ))
- (each [_ event (ipairs (sock:query ))]
- (handle-event event))
-
- {
- :refresh #(each [_ event (ipairs (sock:event))]
- (handle-event event))
- :fd (sock:fd)
- :status (fn [self]
- (self:refresh)
- (let [defaultroute routes.default
- interface (and defaultroute
- (. links defaultroute.index))]
- (and interface (= interface.running "yes")
- (get-network-info interface))))
- :wait #(sock:poll 1000)
- :interface (fn [self ifnum]
- (. links ifnum))
- }
- ))
-
-(comment
-(let [nl (netlunk)]
- (while (or (nl:wait) true)
- (nl:refresh)
- (match (nl:uplink)
- interface
- (print "default route through " (view interface))
- nil
- (print "no default route")
- ))))
-
-{
- :new uplink
- }