#!/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: