The approaches experimented-with so far (later versions do not _necessarily_
obsolete earlier ones, they're just different):
-| Name | Status | Language | Tested-on | Description |
-|--------|--------|-----------|-------------------------|-------------|
-| __x1__ | Works | Bash, AWK | Ubuntu 16.04 | Single, synchronous script, saving state in text files |
-| __x2__ | Works | Bash, AWK | Ubuntu 16.04, Debian 10 | Parallel processes: collectors, cache and reporters; passing messages over pipes |
-| __x3__ | In dev | OCaml | Debian 10 | Re-write and refinement of __x2__ |
+| Name | Status | Language | Tested-on | Description |
+|--------|-----------|-----------|--------------|-------------|
+| __x1__ | Archived | Bash, AWK | Ubuntu 16.04 | Single, synchronous script, saving state in text files |
+| __x2__ | In-use | Bash, AWK | Debian 10 | Parallel processes: collectors, cache and reporters; passing messages over pipes |
+| __x3__ | Scratched | OCaml | Debian 10 | Re-write and refinement of __x2__ |
+| __x4__ | In-dev | Dash, AWK | Debian 10 | Sensors are completely decoupled daemons, cache is a file tree |
--- /dev/null
+X4
+==
+
+- caching directly on the file system
+- sensors are isolated daemons
+
+Status
+------
+
+Began prototyping
--- /dev/null
+#! /bin/bash
+
+set -e
+
+# Defaults
+prefix='/dev/shm/khatus'
+host="$(hostname)"
+sensor="$(basename $0)"
+run_in='foreground' # foreground | background
+run_as='poller' # poller | streamer
+interval=1 # Only relevant if run_as poller, ignored otherwise.
+
+set_common_options() {
+ while :
+ do
+ case "$1"
+ in '')
+ break
+ ;; -d|--daemon)
+ run_in='background'
+ shift 1
+ ;; -i|--interval)
+ case "$2"
+ in '')
+ printf "Option $1 requires and argument\n" >&2
+ exit 1
+ ;; *)
+ interval="$2"
+ shift 2
+ esac
+ ;; *)
+ shift 1
+ esac
+ done
+}
+
+init_dirs() {
+ work_dir="${prefix}/${host}/${sensor}"
+ out_dir="${work_dir}/out"
+ err_file="${work_dir}/err"
+ pid_file="${work_dir}/pid"
+
+ mkdir -p "$out_dir"
+}
+
+streamer() {
+ sensor \
+ | while read key val
+ do
+ printf "%s\n" "$val" > "${out_dir}/${key}"
+ done
+ >> "$err_file"
+}
+
+poller() {
+ while :
+ do
+ streamer
+ sleep "$interval"
+ done
+}
+
+pid_file_create_of_parent() {
+ printf "$$\n" > "$pid_file"
+}
+
+pid_file_create_of_child() {
+ printf "$!\n" > "$pid_file"
+}
+
+pid_file_test() {
+ if test -e "$pid_file"
+ then
+ printf "Error - $sensor already running (i.e. PID file exists at $pid_file)\n" 1>&2
+ exit 1
+ fi
+}
+
+pid_file_remove() {
+ rm -f "$pid_file"
+}
+
+run_in_foreground() {
+ # TODO: Why do INT and EXIT traps only work in combination?
+ trap true INT
+ trap exit TERM
+ trap pid_file_remove EXIT
+ $run_as
+}
+
+run_in_background_2nd_fork() {
+ run_in_foreground &
+ pid_file_create_of_child
+}
+
+run_in_background() {
+ run_in_background_2nd_fork &
+}
+
+run() {
+ case "$run_as"
+ in 'poller' | 'streamer')
+ true
+ ;; *)
+ printf "Error - illegal value for \$run_as: $run_in\n" 1>&2
+ exit 1
+ esac
+ pid_file_test
+ case "$run_in"
+ in 'background')
+ run_in_background
+ ;; 'foreground')
+ pid_file_create_of_parent
+ run_in_foreground
+ ;; *)
+ printf "Error - illegal value for \$run_in: $run_in\n" 1>&2
+ exit 1
+ esac
+}
+
+set_common_options $@
+init_dirs
--- /dev/null
+#! /usr/bin/awk -f
+
+# Msg separator
+/^OK/ {msg_count++; next}
+
+# Msg content
+/^[a-zA-Z-]+: / {
+ key = $1
+ val = $0
+ sub(".*" key " *", "", val)
+ sub(":$", "", key)
+ key = tolower(key)
+ # Note that we expect a particular order of response messages (also
+ # reflected in the name of this script file): "status" THEN "currentsong"
+ if (msg_count == 1) {status[key] = val}
+ else if (msg_count == 2) {currentsong[key] = val}
+ else {
+ printf("Unexpected msg_count in mpd response: %d\n", msg_count) \
+ > "/dev/stderr"
+ exit 1
+ }
+ next
+}
+
+END {
+ name = currentsong["name"]
+ title = currentsong["title"]
+ file = currentsong["file"]
+
+ if (name) {
+ song = name
+ } else if (title) {
+ song = title
+ } else if (file) {
+ last = split(file, parts, "/")
+ song = parts[last]
+ } else {
+ song = "?"
+ }
+
+ format_time(status["time"], time)
+ output["play_time_minimal_units"] = time["minimal_units"]
+ output["play_time_percentage"] = time["percentage"]
+ output["state"] = status["state"]
+ output["song"] = song
+ for (key in output) {
+ print key, output[key]
+ }
+}
+
+function format_time(time_str, time_arr, \
+ \
+ time_str_parts,
+ seconds_current,
+ seconds_total,
+ hours,
+ secs_beyond_hours,
+ mins,
+ secs,
+ time_percentage \
+) {
+ split(time_str, time_str_parts, ":")
+ seconds_current = time_str_parts[1]
+ seconds_total = time_str_parts[2]
+
+ hours = int(seconds_current / 60 / 60);
+ secs_beyond_hours = seconds_current - (hours * 60 * 60);
+ mins = int(secs_beyond_hours / 60);
+ secs = secs_beyond_hours - (mins * 60);
+
+ if (hours > 0) {
+ time_arr["minimal_units"] = sprintf("%d:%.2d:%.2d", hours, mins, secs)
+ } else {
+ time_arr["minimal_units"] = sprintf("%.2d:%.2d", mins, secs)
+ }
+
+ if (seconds_total > 0) {
+ time_percentage = (seconds_current / seconds_total) * 100
+ time_arr["percentage"] = sprintf("%d%%", time_percentage)
+ } else {
+ time_arr["percentage"] = "~"
+ }
+}
--- /dev/null
+#! /usr/bin/awk -f
+
+# When parsing 'upower --dump'
+/^Device:[ \t]+/ {
+ device["path"] = $2
+ next
+}
+
+# When parsing 'upower --monitor-detail'
+/^\[[0-9]+:[0-9]+:[0-9]+\.[0-9]+\][ \t]+device changed:[ \t]+/ {
+ device["path"] = $4
+ next
+}
+
+/ native-path:/ && device["path"] {
+ device["native_path"] = $2
+ next
+}
+
+# BEGIN battery
+/ battery/ && device["path"] {
+ device["is_battery"] = 1
+ next
+}
+
+/ state:/ && device["is_battery"] {
+ device["battery_state"] = $2
+ next
+}
+
+/ energy:/ && device["is_battery"] {
+ device["energy"] = $2
+ next
+}
+
+/ energy-full:/ && device["is_battery"] {
+ device["energy_full"] = $2
+ next
+}
+
+/ percentage:/ && device["is_battery"] {
+ device["battery_percentage"] = $2
+ sub("%$", "", device["battery_percentage"])
+ next
+}
+
+/^$/ && device["is_battery"] {
+ print("battery_state" , aggregate_battery_state())
+ print("battery_percentage", aggregate_battery_percentage())
+}
+# END battery
+
+# BEGIN line-power
+/ line-power/ && device["path"] {
+ device["is_line_power"] = 1
+ next
+}
+
+/ online:/ && device["is_line_power"] {
+ device["line_power_online"] = $2
+ next
+}
+
+/^$/ && device["is_line_power"] {
+ print("line_power", device["line_power_online"])
+}
+# END line-power
+
+/^$/ {
+ delete device
+ next
+}
+
+function aggregate_battery_percentage( bat, curr, full) {
+ _battery_energy[device["native_path"]] = device["energy"]
+ _battery_energy_full[device["native_path"]] = device["energy_full"]
+ for (bat in _battery_energy) {
+ curr = curr + _battery_energy[bat]
+ full = full + _battery_energy_full[bat]
+ }
+ return ((curr / full) * 100)
+}
+
+function aggregate_battery_state( curr, bat, new) {
+ _battery_state[device["native_path"]] = device["battery_state"]
+ curr = device["battery_state"]
+ for (bat in _battery_state) {
+ new = _battery_state[bat]
+ if (new == "discharging") {
+ curr = new
+ } else if (curr != "discharging" && new == "charging") {
+ curr = new
+ }
+ }
+ return curr
+}
--- /dev/null
+#! /bin/sh
+
+set -e
+
+. "$(dirname $(realpath $0))/khatus_x4_lib_common_sensor.sh"
+
+sensor() {
+ printf "datetime $(date +'%a %b %d %H:%M:%S')\n"
+}
+
+run
--- /dev/null
+#! /bin/sh
+
+set -e
+
+bin_dir="$(dirname $(realpath $0))"
+
+. "$bin_dir/khatus_x4_lib_common_sensor.sh"
+
+sensor() {
+ stdbuf -o L upower --dump | stdbuf -o L "$bin_dir"/khatus_x4_parse_upower
+ stdbuf -o L upower --monitor-detail | stdbuf -o L "$bin_dir"/khatus_x4_parse_upower
+}
+
+run_as='streamer'
+run
--- /dev/null
+#! /bin/sh
+
+set -e
+
+bin_dir="$(dirname $(realpath $0))"
+
+. "$bin_dir/khatus_x4_lib_common_sensor.sh"
+
+sensor() {
+ # TODO: Convert mpd sensor to watcher from poller
+ # Since we can just open the connection and send periodic requests.
+ #
+ # close
+ # Closes the connection to MPD. MPD will try to send the remaining output
+ # buffer before it actually closes the connection, but that cannot be
+ # guaranteed. This command will not generate a response.
+ #
+ # Clients should not use this command; instead, they should just close the socket.
+ #
+ # https://www.musicpd.org/doc/html/protocol.html#connection-settings
+ #
+ echo 'status\ncurrentsong\nclose' \
+ | nc 127.0.0.1 6600 \
+ | "$bin_dir"/khatus_x4_parse_mpd_status_currentsong
+}
+
+run
--- /dev/null
+#! /bin/sh
+
+set -e
+
+. ./bin/khatus_x4_lib_common_sensor.sh
+
+dir="${prefix}/${host}"
+
+kill_sensor() {
+ if test -f "$1"
+ then
+ kill $(cat "$1")
+ fi
+}
+
+read_sensor() {
+ if test -f "$1"
+ then
+ cat "$1"
+ else
+ printf '%s\n' '--'
+ fi
+}
+
+kill_sensor ${dir}/khatus_x4_sensor_datetime/pid
+kill_sensor ${dir}/khatus_x4_sensor_mpd/pid
+kill_sensor ${dir}/khatus_x4_sensor_energy/pid
+
+./bin/khatus_x4_sensor_datetime -d
+./bin/khatus_x4_sensor_mpd -d
+./bin/khatus_x4_sensor_energy -d
+
+while :
+do
+ battery_state="$(read_sensor ${dir}/khatus_x4_sensor_energy/out/battery_state)"
+ battery_percentage="$(read_sensor ${dir}/khatus_x4_sensor_energy/out/battery_percentage)"
+ datetime="$(read_sensor ${dir}/khatus_x4_sensor_datetime/out/datetime)"
+ mpd="$(read_sensor ${dir}/khatus_x4_sensor_mpd/out/state)"
+ printf \
+ "E[${battery_state} ${battery_percentage}] [${mpd}] ${datetime}\n"
+ sleep 1
+done