#!/bin/bash
#
################################################################################
#
# This script interacts with the usblcd program from the picoLCD SDK in order
# to provide a fully-featured menu system.
#
# We also want to turn off the backlight after a period of inactivity - but
# only if nothing has been pressed in the intervening period.  The complication
# here is that running a second command against the panel will interrupt the
# existing read command which is looking for input, so this will have to be
# dealt with.
#
################################################################################
#
# Current known bugs:
# * Performing any LCD state-change prevents the listener from receiving any
#   further data, so we either have to wait for a press-and-release before
#   performing any action (such as turning on the screen), or we can simply not
#   de-bounce keypresses.
#
################################################################################

#set -o xtrace

[[ $1 = "__RELAUNCH__" ]] || exec $0 __RELAUNCH__ >/dev/null 2>&1

function die() {
	echo "$@"
	exit 1
}

name="$( basename $0 )"
name="${name/.*/}"

function debug() {
	local debug=0

	(( debug )) && echo "$( date ) DEBUG: $@" | tee -a /var/log/$name.log
}

path=/usr/local/bin
usblcd=usblcd

sleep=1
delay=5

tristate=0
up="."     # .,    ^, n, o - There are entries above
down="."   # ., V, v, u, o - There are entries below
top="n"    # .,    ^, n, o - Top entry
mid="H"    #       |, H, o - Middle entries
btm="u"    # ., V, v, u, o - Bottom entry
left="<<"  # Must be two characters
right=">>" # Must be two characters

tmp=/dev/shm

BACKLIGHT=1
KEYPAD=0
F1=0
F2=0
F3=0
F4=0
F5=0
MENU=0
LAST="U"
REVERT=0
CLOCK=1

[[ -x $path/$usblcd ]] || die "FATAL: \"$path/$usblcd\" not found"
[[ -d $tmp && -w $tmp ]] || die "FATAL: Cannot write to \"$tmp\""

debug "Starting ($$)"

rm $tmp/.$name.* >/dev/null 2>&1

function updatestate() {
	date -u +%s > $tmp/.$name.state
} # updatestate

function savestate() {
	echo "$BACKLIGHT" > $tmp/.$name.state.bl
	echo "$KEYPAD" > $tmp/.$name.state.kp
	echo "$F1" > $tmp/.$name.state.f1
	echo "$F2" > $tmp/.$name.state.f2
	echo "$F3" > $tmp/.$name.state.f3
	echo "$F4" > $tmp/.$name.state.f4
	echo "$F5" > $tmp/.$name.state.f5
	echo "$MENU" > $tmp/.$name.state.mu
}

function loadstate() {
	[[ -r $tmp/.$name.state.bl ]] && BACKLIGHT="$( cat $tmp/.$name.state.bl 2>/dev/null )"
	[[ -r $tmp/.$name.state.kp ]] && KEYPAD="$( cat $tmp/.$name.state.kp 2>/dev/null )"
	[[ -r $tmp/.$name.state.f1 ]] && F1="$( cat $tmp/.$name.state.f1 2>/dev/null )"
	[[ -r $tmp/.$name.state.f2 ]] && F2="$( cat $tmp/.$name.state.f2 2>/dev/null )"
	[[ -r $tmp/.$name.state.f3 ]] && F3="$( cat $tmp/.$name.state.f3 2>/dev/null )"
	[[ -r $tmp/.$name.state.f4 ]] && F4="$( cat $tmp/.$name.state.f4 2>/dev/null )"
	[[ -r $tmp/.$name.state.f5 ]] && F5="$( cat $tmp/.$name.state.f5 2>/dev/null )"
	[[ -r $tmp/.$name.state.mu ]] && MENU="$( cat $tmp/.$name.state.mu 2>/dev/null )"
}

function updatelcd() {
	local update=1

	updatestate
	if [[ $1 = "_noupdate_" ]]; then
		update=0
		shift
	fi

debug "updatelcd $update $BACKLIGHT$KEYPAD$F1$F2$F3$F4$F5 $@"

	$path/$usblcd backlight $BACKLIGHT led 0 $KEYPAD \
		led 1 $F1 led 2 $F2 led 3 $F3 led 4 $F4 led 5 $F5 "$@" >/dev/null 2>&1
	savestate
	(( update )) && pkill -HUP $usblcd
} # updatelcd

function lcdtext() {
	local last locktime state

	[[ -e "$tmp/.$name.message" ]] && \
		last="$( tail -n 1 "$tmp/.$name.message" )"

	#local text="$@"
	local text="$( echo "$@" | sed "s/_BLANK_/                   /g" )"
	if (( ${#text} > 21 )) && [[ ${text:20:1} = " " ]]; then
		text="${text:0:20}${text:21}"
	fi

	if [[ "_CLEAR_" = "${text}" ]]; then
		[[ -e "$tmp/.$name.message" ]] && \
			rm "$tmp/.$name.message" >/dev/null 2>&1
		updatelcd clear
		return
	fi

	local length
	(( length = ${#text} ))
	if (( 20 == length )); then
		[[ -n "$tmp" ]] && echo "$text" > "$tmp/.$name.message" 2>/dev/null
		if [[ -n "$last" ]]; then
			updatelcd _noupdate_ text 1 0 "$text"
			updatelcd text 0 0 "$last"
			return
		else
			updatelcd text 0 0 "$text"
			return
		fi
	elif (( 20 > length )); then
		local pos count

		(( count = 20 - length ))
		for pos in $( seq $count -1 1 ); do
			text="$text "
		done
		[[ -n "$tmp" ]] && echo "$text" > "$tmp/.$name.message" 2>/dev/null
		if [[ -n "$last" ]]; then
			updatelcd _noupdate_ text 1 0 "$text"
			updatelcd text 0 0 "$last"
			return
		else
			updatelcd text 0 0 "$text"
			return
		fi
	elif (( 20 < length )); then
		local pos count
		local line0 line1

		line0="${text:0:20}"
		line1="${text:20:20}"
		(( count = 20 - ${#line1} ))
		for pos in $( seq $count -1 1 ); do
			line1="$line1 "
		done
		[[ -n "$tmp" ]] && echo "$line1" > "$tmp/.$name.message" 2>/dev/null
		updatelcd _noupdate_ text 1 0 "$line1"
		updatelcd text 0 0 "$line0"
		return
	fi
} # lcdtext

function lc() {
	local text="$@"

	echo "$text" | tr [A-Z] [a-z]
} # lc

function uc() {
	local text="$@"

	echo "$text" | tr [a-z] [A-Z]
} # uc

function pad() {
	local text pos length=20
	text="$1"
	shift
	[ -n "$1" ] && length=$1
	(( length > 20 )) && length=20

	for pos in $( seq ${#text} $length ); do
		text="$text "
	done

	echo "${text:0:$length}"
} # pad

function centre() {
	# In the 'seq' commands below, a starting
	# value of '0' right-aligns text with an
	# odd number of characters, whilst '1'
	# left-aligns it.
	local text pos count
	text="$@"
	(( count = 20 - ${#text} ))
	for pos in $( seq 1 2 $count ); do
		text=" $text "
	done

	echo "${text:0:20}"
} # centre

function action_info() {
	local hostname="$( uname -n )"
	local ipaddr="$( /sbin/ifconfig | grep 'inet addr' | grep -v '127.0.0.1' | \
		cut -d':' -f 2 | cut -d' ' -f 1 | head -n 1 )"

	lcdtext "$( centre "$hostname" )" "$( centre "$ipaddr" )"
} # action_info

function watcher() {
	local finished state f1 f2 f3 f4 f5

	finished=0
	while ! (( finished )) ; do
		if ! [[ -e $tmp/.$name.lock ]]; then
			echo "watcher" > $tmp/.$name.lock
			sleep 0.1
			if [[ "watcher" = "$( cat $tmp/.$name.lock 2>/dev/null )" ]]; then
				if [[ -e $tmp/.$name.state ]]; then
					(( state = $( cat $tmp/.$name.state 2>/dev/null ) ))
					if (( state + delay < $( date -u +%s ) )); then
						BACKLIGHT=0
						KEYPAD=0
						f1=$F1
						f2=$F2
						f3=$F3
						f4=$F4
						f5=$F5
						F1=0
						F2=0
						F3=0
						F4=0
						F5=0
						MENU=0
						[[ -e $tmp/.$name.state.noclear ]] || \
						{ (( REVERT )) && action_info || updatelcd ; }
						F1=$f1
						F2=$f2
						F3=$f3
						F4=$f4
						F5=$f5
						savestate
						rm $tmp/.$name.state >/dev/null 2>&1
					fi
				fi
				rm $tmp/.$name.lock >/dev/null 2>&1
			fi
		fi
		if ! [[ -n "$( ps | grep $$ | grep -v grep )" ]]; then
			[[ -e $tmp/.$name.state.noclear ]] || \
				$path/$usblcd backlight 0 led 0 0 \
					led 1 0 led 2 0 led 3 0 led 4 0 led 5 0 clear >/dev/null 2>&1
			debug "Stopped ($$)"
			pkill -HUP $usblcd
			exit 0
		fi
		sleep $sleep
	done
} # watcher

function listener() {
	local reboot=0
	local released=1
	local beep=0

	type -pf beep >/dev/null 2>&1 && beep=1

	function action_noop() {
		return
	}

	function action_uptime() {
		local line0 line1
		line0="System running:"
		line1="$( uptime | tr -s [:space:] | cut -d" " -f 3- | cut -d"," -f 1-2 )"
		if $( echo "$line1" | grep ":" >/dev/null 2>&1 ); then
			line1="$( echo "$line1" | sed 's/:/h / ; s/$/m/ ; s/^up //' )"
		else
			line1="$( echo "$line1" | sed 's/min/minutes/ ; s/^up //' )"
		fi
		lcdtext "$( pad "$line0" )" "$( pad "$line1" )"
	}

	function action_loadavg() {
		line0="Load average:"
		line1="$( uptime | sed 's/^.*load average: //' )"
		lcdtext "$( pad "$line0" )" "$( pad "$line1" )"
	}

	function action_reboot() {
		F1=1
		F5=1
		reboot=1
		#        01234567890123456789
		lcdtext "Press F1+F5, then F3" "$( centre "to reboot" )"
	}

	function action_shutdown() {
		#        012345 678901 23456789
		lcdtext "Press \"power\" button" "$( centre "to shutdown" )"
	}

	function action_about() {
		lcdtext "$( centre "Copyright (C)2008" )" "$( centre "Stuart Shelton" )"
	}

	function handle_events() {
		local key1 key2 line0 line1
		local input="$@"

		if [[ ${input:0:3} = "Key" ]]; then
			key1="${input:5:2}"
			key2="${input:8:2}"

			# Debouncing isn't reliable (because we keep killing the usblcd
			# process whenever we make a change), so let's drop it.
			# We get a notification on every press, in any case...
			if [[ $key1 = "00" ]] && [[ $key2 = "00" ]]; then
				released=1
				(( BACKLIGHT )) && return
			else
				released=0
			fi
			(( beep )) && beep -f 900 -l 10
			type -pf beep >/dev/null 2>&1 && beep=1

debug "read key1 \"$key1\", key2 \"$key2\"; reboot is $reboot"

			# Keys:
			# +/-     : 01,  02
			# F1 - F5 : 03 - 07
			# Up/Down : 0a,  0b (10, 11)
			# Lft/Rght: 08,  09
			# Enter   : 0c      (12)

			# Lights:
			# Keypad  :  0
			# F1 - F5 :  1 -  5

			if (( 0x$key1 < 0x$key2 )); then
				debug "WARN: Logic error, key1 not greatest value"
			fi

			# Redraw the display following processing?
			local update=1

			if (( reboot == 2 )); then
				if (( 0x$key1 == 5 )); then # F3
					KEYPAD=0 F3=0 lcdtext "_BLANK_" "Rebooting..."
					# Danger, Will Robinson!
					touch $tmp/.$name.state.noclear
					shutdown -r now
				elif (( ( 0x$key1 == 7 || 0x$key1 == 3 ) && ! released )); then
					updatestate
				else
					reboot=0
					# Synthesise event
					MENU=1 KEYPAD=1 F1=0 F3=0 F5=0 beep=0 handle_events "Key: $key1 $key2"
				fi
			elif (( reboot )); then
				if (( 0x$key1 == 7 )) && (( 0x$key2 == 3 )); then # F5 & F1
					line0="$( centre "Reboot?" )"
					line1="$( centre "Press F3 to confirm" )"
					reboot=2
					KEYPAD=0 F1=0 F3=1 F5=0 lcdtext "$line0" "$line1"
				elif (( ( 0x$key1 == 7 || 0x$key1 == 3 ) && ! released )); then
					updatestate
				else
					reboot=0
					# Synthesise event
					MENU=1 KEYPAD=1 F1=0 F3=0 F5=0 beep=0 handle_events "Key: $key1 $key2"
				fi
			else
				reboot=0
				F1=0
				F3=0
				F5=0
				case $(( 0x$key1 )) in
					8)	# Left
							if (( MENU )); then
								if (( menu_x <= 0 )); then
									if (( wrap )); then
										(( menu_x = ${#titles[@]} - 1 ))
										(( menu_y = -1 ))
										LAST="U"
									fi
								else
									(( menu_x-- ))
									(( menu_y = -1 ))
									LAST="U"
								fi
							else
								MENU=1
							fi
							;;
					9)	# Right
							if (( MENU )); then
								if (( menu_x >= ${#titles[@]} - 1 )); then
									if (( wrap )); then
										(( menu_x = 0 ))
										(( menu_y = -1 ))
										LAST="U"
									fi
								else
									(( menu_x++ ))
									(( menu_y = -1 ))
									LAST="U"
								fi
							else
								MENU=1
							fi
							;;
					10)	# Up
							if (( MENU )); then
								# Titles appear at y=0, so we actually need to check for <= -1
								if (( menu_y <= -1 )); then
									if (( wrap )); then
										(( menu_y = $( eval echo "\${#menu_$menu_x[@]}" ) - 1 ))
										LAST="D"
									fi
								else
									(( menu_y-- ))
									LAST="U"
								fi
							else
								MENU=1
							fi
							;;
					11) # Down
							if (( MENU )); then
								if (( menu_y >= $( eval echo "\${#menu_$menu_x[@]}" ) - 1 )); then
									if (( wrap )); then
										menu_y=0
										LAST="U"
									fi
								else
									(( menu_y < 0 )) && menu_y=0
									(( menu_y++ ))
									LAST="D"
								fi
							else
								MENU=1
							fi
							;;
					12) # Enter
							if (( MENU )); then
								local index=$menu_y
								(( index < 0 )) && (( index = 0 ))
								local func="$( eval echo "\${func_$menu_x[$index]}" )"
								unset index
								if [[ "$( type -t $func )" = "function" ]]; then
									MENU=0
									update=0
									$func
								else
									debug "Missing function '$func'"
								fi
								unset func
							else
								MENU=1
							fi
							;;
				esac
				debug "menu_x is $menu_x, menu_y is $menu_y, LAST is $LAST"
				if (( update )); then
					if (( MENU )); then
							BACKLIGHT=1
							KEYPAD=1
							if (( $( eval echo "\${#menu_$menu_x[@]}" ) == 1 )); then
								# This menu is only a single item long!
								line0="$( centre "${titles[$menu_x]}" )"
								line0="${line0:3:14}"
								line1="-> $( eval echo "\${menu_$menu_x[0]}" )"
								if (( wrap )); then
									line0="$left $line0 $right"
								else
									(( menu_x == 0 )) && line0="   $line0" || line0="$left $line0"
									(( menu_x == ${#titles[@]} - 1 )) && line0="$line0   " || line0="$line0 $right"
								fi
							else
								case "$LAST" in
									U)	if (( menu_y == -1 )); then
												line0="$( centre "${titles[$menu_x]}" )"
												line0="${line0:3:14}"
												if (( wrap )); then
													line0="$left $line0 $right"
												else
													(( menu_x == 0 )) && line0="   $line0" || line0="$left $line0"
													(( menu_x == ${#titles[@]} - 1 )) && line0="$line0   " || line0="$line0 $right"
												fi
												# Due to the condition above, there will always be a next item here
												if (( tristate )); then
													line1="$( pad "-> $( eval echo "\${menu_$menu_x[0]}" )" 18 ) $top"
												else
													line1="$( pad "-> $( eval echo "\${menu_$menu_x[0]}" )" 18 ) $down"
												fi
											else
												line0="$( pad "-> $( eval echo "\${menu_$menu_x[$menu_y]}" )" 18 )"
												line1="$( pad "   $( eval echo "\${menu_$menu_x[$(( menu_y + 1 ))]}" )" 18 )"
												if (( tristate )); then
													(( menu_y < 1 )) && line0="$line0 $top" || line0="$line0 $mid"
													(( menu_y + 1 ==  $( eval echo "\${#menu_$menu_x[@]}" ) - 1 )) && line1="$line1 $btm" || line1="$line1 $mid"
												else
													(( menu_y > 0 )) && line0="$line0 $up" || line0="$line0  "
													(( menu_y + 1 < $( eval echo "\${#menu_$menu_x[@]}" ) - 1 )) && line1="$line1 $down" || line1="$line1  "
												fi
											fi
											;;
									D)	line0="$( pad "   $( eval echo "\${menu_$menu_x[$menu_y - 1 ]}" )" 18 )"
											line1="$( pad "-> $( eval echo "\${menu_$menu_x[$menu_y]}" )" 18 )"
											if (( tristate )); then
												(( menu_y -1 < 1 )) && line0="$line0 $top" || line0="$line0 $mid"
												(( menu_y ==  $( eval echo "\${#menu_$menu_x[@]}" ) - 1 )) && line1="$line1 $btm" || line1="$line1 $mid"
											else
												(( menu_y - 1 > 0 )) && line0="$line0 $up" || line0="$line0  "
												(( menu_y < $( eval echo "\${#menu_$menu_x[@]}" ) - 1 )) && line1="$line1 $down" || line1="$line1  "
											fi
											;;
								esac
							fi
							lcdtext "$line0" "$line1"
							updatestate
					elif ! (( BACKLIGHT )) || ! (( KEYPAD )); then
						BACKLIGHT=1
						KEYPAD=1
						updatelcd
					else
						updatestate
					fi
				fi
			fi
		fi
	} # handle_events

	action_info

	# Set up menu structure
	# Fetch these my setting 'menu' and then using "${!menu}"
	local titles menu_0 menu_1 menu_2 menu_x=0 menu_y=-1 wrap=0
	if (( wrap )); then
		up=" "
		down=" "
	fi
	titles=( "Status" "System" "About" )
	menu_0=( "Information" "Uptime" "Load Average" )
	menu_1=( "Reboot" "Shutdown" )
	menu_2=( "Credits" )
	func_0=( action_info action_uptime action_loadavg )
	func_1=( action_reboot action_shutdown )
	func_2=( action_about )

	local line
	while true; do
		loadstate

debug "listener $BACKLIGHT$KEYPAD$F1$F2$F3$F4$F5 $@"

		while read line; do
			handle_events $line
		done < <( $path/$usblcd backlight $BACKLIGHT led 0 $KEYPAD \
							led 1 $F1 led 2 $F2 led 3 $F3 led 4 $F4 led 5 $F5 read 2>&1 ) >/dev/null 2>&1
	done
} # listener

watcher &
listener
exit 0

# vi: set ts=2:
