Battery status over i.MX and LPC

Edit: Made the script a little more efficient about how it parses out the fields from the battery info string and added support for $amps and $volts in the alert command specs.

Edit 2: I’ve actually reduced the polling to 60 seconds just now as I have begun seeing some weird responses from the LPC after running the service for a while. Not sure what’s going on there, but my first guess here is that I’m over-taxing whatever circuitry is involved with my polling every two seconds. Will report back if this problem continues to occur.

Edit 3: Still seeing the strange responses sometimes. They look like this:

j0328mV25750  59%

All the information I want is there, so I could work around it in my script, but I’d rather get at the root cause.

Okay, I’ve got an approach for tracking my battery charge status that I’m pretty happy with for now.

I’m running this script as a systemd service. It’s an evolution of the script above. It only sends the “redirect output” command once when it starts and then the “un-redirect output” command once when it’s killed. It polls the battery state periodically and is meant to be long-running. It also allows omitting either or both of the first two “alert” fields, which lets you specify a command to run when a given percentage is hit or every time the script polls the battery, regardless of whether you’re discharging or charging. You can also reference the battery state and charge percentage, as well as the original line returned by the LPC, in the command string.

Here it is. I’ve got it installed at /usr/local/sbin/reform-battery-charging-alert-service.sh:

#!/bin/sh

set -e

usage="Usage: $0 [OPTIONS]

Options:
  -h           Display this message and exit with error code 0.

  -p <seconds> How long to wait between polls. Defaults to 60 seconds.

  -a <spec>    An alert specification. This flag may be set multiple times. Alert
               specifications are in this format:
               \"[charging|discharging]:[percent charge]:<command>\"

	       The following variables can be referenced in the <command> string:
	       - \$state: Either \"charging\" or \"discharging\".
	       - \$amps: Battery charge as a percentage.
	       - \$volts: Battery voltage, unformatted.
	       - \$percent: Battery charge as a percentage.
	       - \$bat_info: The raw battery status string returned by the LPC.
	    
	       Example:
	       $0 \\
	         -a 'discharging:25:notify-send \"Discharged to 25%!\"' \\
	         -a 'charging:75:notify-send \"Charged to 75%!\"' \\
	         -a 'charging::notify-send \"Polled the battery while charging!\"' \\
	         -a ':50:notify-send \"Battery charged or discharged to 50%!\"' \\
	         -a '::notify-send \"Polled! Battery at \$percent% and \$state.\"'
"

[ "$1" ] || { >&2 echo "$usage"; exit 1;  }

poll_pause=60
alerts=""

expect_value() {
	if [ ! "$2" ]; then
		>&2 echo "Missing value for flag '$1'!"
		exit 1

	elif [ "$2" != "${2#-}" ]; then
		>&2 echo "Invalid value for flag '$1': '$2'"
		exit 1
	fi
}

while [ "$1" ]; do
	case "$1" in
		-h)
			echo "$usage"
			exit 0
			;;
			
		-p)
			expect_value "$1" "$2"
			poll_pause="$2"
			shift
			;;

		-a)
			expect_value "$1" "$2"

			if ! printf '%s' "$2" \
				| grep -q '^\(\(dis\)\?charging\)\?:\([0-9]\{1,3\}\)\?:.\+$' 
			then
				>&2 echo "Invalid alert specification:"
				>&2 printf "%s\n" "$2"
				exit 1
			fi

			target="$(printf '%s' "$2" | cut -d: -f2)"
			if [ "$target" ] && [ "$target" -gt 100 ]; then
				>&2 echo "Target charge percentage >100 in specification:"
				>&2 printf "%s\n" "$2"
				exit 1
			fi

			# Accumulate all alert specifications, keeping track of how many
			# bytes long each is.
			alerts="${alerts}$(printf '%s' "$2" | wc -c):${2}"
			shift
			;;

		*)
			>&2 echo "Unrecognized option: $1"
			exit 1
			;;
	esac

	shift
done

# Set the baud rate on ttymxc2 to 57600
stty -F /dev/ttymxc2 57600

# Redirect the LPC's serial output to /dev/ttymxc2
echo "xUAR1" > /dev/hidraw0

# Stop redirecting the output after the script ends for whatever reason.
trap 'sleep 1; echo "xUAR0" > /dev/hidraw0' EXIT HUP INT QUIT TERM STOP PWR

# Wait a couple seconds for the redirect command to apply and its output to clear.
sleep 2

# Loops until a kill signal is received.
while true; do
	bat_info=""

	while [ ! "$bat_info" ]; do
		# Request battery status.
		echo "xRPRT" > /dev/hidraw0

		# Read the LPC's response.
		export bat_info="$(head -n 1 /dev/ttymxc2)"

		if [ ! "$bat_info" ]; then
			>&2 echo "Error trying to retrieve battery information. Pausing before retrying..."
			sleep "$poll_pause"
		fi
	done

	# Parse the LPC's battery info line.
	# This line will split each field into our argument array.
	eval "set -- $(echo "$bat_info" | sed 's/m\(V\|A\)/ /g')"
	export amps="$(echo "$9" | sed 's/\(-\?.\)\(...\)/\1.\2/')"
	export volts="$(echo "${10}" | sed 's/\(..\)\(...\)/\1.\2/')"
	export percent="${11%\%}"

	if [ "${amps}" != "${amps#-}" ]; then
		state="charging"
	else
		state="discharging"
	fi
	export state

	alerts_parsing="$alerts"

	while [ "$alerts_parsing" ]; do
		strlen="${alerts_parsing%%:*}"
		alerts_parsing="${alerts_parsing#*:}"
		alert="$(printf '%s' "$alerts_parsing" | dd bs=1 count=$strlen 2> /dev/null)"
		alerts_parsing="$(printf '%s' "$alerts_parsing" | dd bs=1 skip=$strlen 2> /dev/null)"

		if [ "${alert}" = "${alert#:}" ] && [ "${alert}" = "${alert#$state}" ]; then
			continue
		fi

		target="$(printf '%s' "$alert" | cut -d: -f2 | tr -d ' ')"

		if [ "$target" ]; then
			if [ ! "$cached_percent" ] && [ "$percent" != "$target" ]; then
				continue;
			fi

			if [ "$state" = "charging" ] && \
				{ [ "$cached_percent" -ge "$target" ] || [ "$percent" -lt "$target" ]; }
			then
				continue
			fi

			if [ "$state" = "discharging" ] && \
				{ [ "$cached_percent" -le "$target" ] || [ "$percent" -gt "$target" ]; }
			then
				continue
			fi
		fi

		# Execute the alert command
		echo "$(printf '%s' "$alert" | cut -d: -f3-)" | sh
	done

	cached_percent="$percent"
	sleep "$poll_pause"
done

As the name suggests, I’ve got it installed as a systemd service now. Here’s /etc/systemd/system/reform-battery-alert.service:

[Unit]
Description=Reform Battery Alerts

[Service]
Type=simple
ExecStartPre=touch /dev/shm/battery && chmod 644 /dev/shm/battery
ExecStart=/usr/local/sbin/reform-battery-charging-alert-service.sh \
	-p 60 \
	-a 'discharging:25:/usr/local/sbin/reform-oled-text.sh -c "" "   Battery at 25%!" "   Time to charge."' \
        -a 'charging:75:/usr/local/sbin/reform-oled-text.sh -c "" "   Battery at 75%!" "   Time to unplug."' \
        -a '::echo "$percent\t$state" > /dev/shm/battery'
ExecStopPost=rm /dev/shm/battery

[Install]
WantedBy=multi-user.target

Might reduce the polling frequency (set to 2 seconds in that service file), might not. It’s not an especially heavy script. Polling frequency is currently to once every 60 seconds in that service file.

I’m writing the charge percentage and charging state to /dev/shm/battery in that file to allow multiple scripts to check my battery’s charge state and percentage. Thought about using a named pipe, but didn’t want to deal with any confusion later on if the script’s output gained a consumer.

Currently the sole consumer is a script I’m running from yambar to keep tabs on my charge percentage and charging state, ~/intramuros/scripts/battery-yambar.sh":

#!/bin/sh

percent="$(cut -f1 /dev/shm/battery)%"
state="$(cut -f2 /dev/shm/battery)"

if [ "$state" = "charging" ]; then
	percent="⚡ $percent"
fi
echo "battery|string|$percent"
echo

Which is referenced in ~/.config/yambar/config.yml like so:

...
  right:
...
    - script:
        path: "/home/lykso/intramuros/scripts/battery-yambar.sh"
        args: []
        content: {string: {text: "{battery}"}}
        poll-interval: 2
...

As you might have noticed, I’m also using @salsaalibi’s script for writing text to my OLED screen when I reach 75% discharged and 75% charged. It was originally shared here. I’m including it here as well for completeness:

#!/bin/bash

# In order to run this script, a user needs to have permission to write to /dev/hidraw0. One way
# to do this is to create a udev rule to set the device's group to a group that the user belongs
# to. For example, this command will create a udev rule that sets the group on all hidraw devices
# to 'dialout' and sets their permissions to 660.
#
# $ sudo echo SUBSYSTEM=="hidraw", GROUP="dialout", MODE="0660" > /etc/udev/rules.d/50-hidraw.rules
#
# After creating the rule, restart the machine to reload the device with the new properties.
#
# Make sure to add the user to the group you used, if they aren't already a member of that group.
# See /etc/group for current group membership and the adduser command for assigning a user to a group.


help()
{
	echo "Write some text to the OLED display on MNT Reform."
	echo "Usage: oledmsg [OPTIONS] [TEXT]"
	echo
	echo "The OLED display is 4 lines tall. Each line is 21 characters long."
	echo "If TEXT is omitted, oledmsg will clear the OLED display."
	echo
	echo "Options:"
	echo "	-h	Print this help."
	echo "	-j	Print all text arguments without line breaks. This is the default."
	echo "	-l	Start a new line between each text argument. Long lines will wrap."
	echo "	-c	Start a new line between each text argument. Long lines will be clipped."
	exit
}

JOIN_MODE=0
BREAK_MODE=1
CLIP_MODE=2
MODE=$JOIN_MODE

while getopts :cjl FLAG; do
	case $FLAG in
		c) # line break between args and clip long lines
			MODE=$CLIP_MODE
			;;
		l) # line break between args and wrap long lines
			MODE=$BREAK_MODE
			;;
		\?) # unknown option
			help
			;;
	esac
done

# Shift argument pointer to the end of option arguments (start of text arguments).
shift $((OPTIND-1))

# Build a string according to the chosen mode.
if [ $MODE -eq $BREAK_MODE ]; then
	for ARG in "$@"; do
		for (( i=0; i < ${#ARG}; i+=21 )); do
			MSG+=$(printf "%-21s" "${ARG:(i):21}")
		done
	done
	MSG=$(printf "%-84s" "${MSG}")
elif [ $MODE -eq $CLIP_MODE ]; then
	for ARG in "$@"; do
		MSG+=$(printf "%-21s" "${ARG:0:21}")
	done
	MSG=$(printf "%-84s" "${MSG}")
elif [ $MODE -eq $JOIN_MODE ]; then
	ARGJOIN="$@"
	MSG=$(printf "%-84s" "${ARGJOIN}")
else
	help
fi

# Write the formatted message to the OLED display.
echo "xOLED${MSG:0:84}" > /dev/hidraw0
2 Likes