#!/bin/bash
#
#  Copyright 2016 Guralp Systems Limited.
#  Author:   R.J.Dunlop  <rdunlop@guralp.com>
#
# Unified State of Health interface script.
# Only two devices use the uSoH.  3T OBS varients and the OBS autonomous
# leveller.
#
# Exit codes:
#  0  Success.
#  1  Parameter error or sanity checks.
#  2  Main action script (expect) not run due to errors.
#  3  Fatal error from action script.
#  4  Error from SoH/leveller.
#
export PATH="/usr/sbin:/usr/bin:/sbin:/bin"

program_name=$(basename $0)

action=""
port_name=""
hexfile=""

power1_name=""
power1_before=""
power2_name=""
power2_before=""

mode_debug=false
mode_force=false
mode_silent=false
mode_progress=false

# Display usage for command line users

usage()
{
    cat 1>&2 <<EOF

usage: $program_name [options...] action port [power1 [power2]]

options:
    --help      This message.
    --file #    File to upload.
    --debug	Enable debugging output.
    --force     Ignore various sanity checks.
    --silent	Minimum output.
    --progress	Progress messages (formatted for Pt-web interpretation).

actions:
    upload	Upload a new firmware image.  Requires --file option.
    lock	Lock sensor for transport.
    unlock	Unlock the sensor.
    center	Center the sensor.
    level	Start the levelling operation.
    hunt	Tell leveller to retry around existing location.
    datum	Return leveller to the datum position.
    soh_status	3 axis states plus sensor tilt.
    lev_status	Leveller body tilt.

EOF
}


# Display a sanity check failure and exit

sanity_chk()
{
    local ecode=$1
    shift

    $mode_silent || echo "$*" 1>&2
    $mode_force || exit $ecode
}


# Main program starts here
# ------------------------

# Load and check the command line parameters

while true
do
    case "$1" in
    -h|--help)		usage ; exit 0			;;
    -f|--file)		hexfile="$2" ; shift		;;
    --debug)		mode_debug=true			;;
    --force)		mode_force=true			;;
    --silent)		mode_silent=true		;;
    --progress)		mode_progress=true		;;
    -*)			echo "Unknown option $1" 1>&2 ; usage ; exit 1 ;;
    *)			break				;;
    esac
    shift
done

case $# in
4)
    power2_name="$4"
    power1_name="$3"
    port_name="$2"
    action="$1"
    ;;
3)
    power1_name="$3"
    port_name="$2"
    action="$1"
    ;;
2)
    port_name="$2"
    action="$1"
    ;;
*)
    usage
    exit 1
    ;;
esac

# Sanity checks

case "X$action" in
Xupload|Xlock|Xunlock|Xcenter|Xlevel|Xhunt|Xdatum|Xsoh_status|Xlev_status)
    ;;
Xcentre)
    action=center
    ;;
*)
    sanity_chk 1 "Unknown action $action"
    action=""
    ;;
esac

if [ -n "$port_name" ]
then
    if [ ! -r "/etc/conf.d/serial.local/${port_name}.cf" ]
    then
	sanity_chk 1 "Missing or unreadable port configuration"
	port_name=""
    fi
else
    sanity_chk 1 "Missing port name"
fi

if [ -n "$hexfile" ]
then
    if [ ! -r "$hexfile" ]
    then
	sanity_chk 1 "Source file $hexfile does not exist?"
	hexfile=""
    fi
else
    if [ "$action" == "upload" ]
    then
	sanity_chk 1 "Missing upload hex file"
	action=""
    fi
fi

# Grab existing power state as well as further sanity checks
if [ -n "$power1_name" ]
then
    power1_before="$(ioline --line "$power1_name" --q-level)"
    if [ $? != 0 ]
    then
	sanity_chk 1 "Missing power control $power1_name"
	power1_name=""
    fi
fi
if [ -n "$power2_name" ]
then
    power2_before="$(ioline --line "$power2_name" --q-level)"
    if [ $? != 0 ]
    then
	sanity_chk 1 "Missing power control $power2_name"
	power2_name=""
    fi
fi

# Turn power on if not already on
powered_up=false
if [ -n "$power1_name" -a "$power1_before" == "0" ]
then
    ioline --line "$power1_name" --q-output 1
    if [ $? != 0 ]
    then
	$mode_silent || echo "Failed to turn $power1_name on" 1>&2
    else
	powered_up=true
    fi
fi
if [ -n "$power2_name" -a "$power2_before" == "0" ]
then
    ioline --line "$power2_name" --q-output 1
    if [ $? != 0 ]
    then
	$mode_silent || echo "Failed to turn $power2_name on" 1>&2
    else
	powered_up=true
    fi
fi
$powered_up && sleep 5


# Expect script starts here

if [ -n "$action" -a -n "$port_name" ]
then
    expect <<EOF

	proc fatal { ecode str } {
	    if { "$mode_silent" != "true" } {
		send_user "\n\$str\n"
	    }
	    exit \$ecode
	}

	proc tell_user { str } {
	    if { "$mode_silent" != "true" } {
		send_user "\$str"
	    }
	}

	# Forward a progress report once every 5 seconds or so.
	# We filter the reports and only report an incrementing progress
	# percentage.  On multi-stage operations where the percentage can go
	# backwards between stages, or when a percentage is not being reported
	# we increment by 1% every 5s to keep the display active until the
	# input catches us up.
	#
	set last_progress [clock seconds]
	set progress_percent 0
	set making_progress 0

	proc send_progress { str } {
	    global last_progress
	    global progress_percent
	    global making_progress

	    if { "$mode_progress" == "true" } {
		# Grab any new percentage figure
		if { [regexp {([0-9]+)%} "\$str" dummy_match this_percent] } {
		    if { \$this_percent > \$progress_percent } {
			set progress_percent \$this_percent
			set making_progress 1
		    }
		}

		# Only send one message every 5s
		set tnow [clock seconds]
		if { \$tnow >= \$last_progress + 5 } {
		    if { ! \$making_progress } {
			incr progress_percent
		    }
		    send_user "Running: \$progress_percent%\n"
		    set last_progress \$tnow
		    set making_progress 0
		}
	    }
	}

	# This is a simplistic file uploader.
	# We send one line of text at a time and wait for the 8 character
	# hex response.  We don't compute or check the response value.
	# "Transfer complete" terminates the transfer, timeout or any other
	# message with a space in it is an error.
	#
	proc send_hex_file { file } {
	    set f [open \$file r]
	    set timeout 10
	    while { [gets \$f line] >= 0 } {
		send -- "\$line\r"
		expect {
		    -re "\[0-9A-Fa-f\]{8}\r" {
			tell_user "."
		    }
		    "Transfer complete" {
			tell_user "\nTransfer complete\n"
			break
		    }
		    # Error responses have spaces in them
		    -re ".* .*\r" {
			fatal 4 "Transfer response \$expect_out(buffer)"
		    }
		    timeout {
			fatal 3 "Transfer stalled"
		    }
		}
	    }
	    close \$f
	    tell_user "Done\n"
	}

	proc issue_command { prompt first cmd } {
	    expect -re \$

	    if { "\$first" == "true" } {
		set timeout 5

		send -- "\003"
		sleep 0.1
		send -- "\003"
	    } else {
		set timeout 2

		send -- "\r"
	    }

	    expect {
		"\$prompt > " { send "\$cmd\r" ; expect "\$cmd\r" }
		"Diag > "     { send "\$cmd\r" ; expect "\$cmd\r" }
		"Factory > "  { send "\$cmd\r" ; expect "\$cmd\r" }
		timeout {
		    fatal 3 "No prompt issuing \$cmd command"
		}
	    }
	}

	proc wait_for_completion { prompt } {
	    global timeout
	    set timeout 10

	    expect {
	    -re "Running: \(\[^\\r\]*\)\\r" {
			send_progress "\$expect_out(1,string)"
			exp_continue
		}
	    -re "Debug: \[^\\r\]*\\r" {
			exp_continue
		}
	    -re "Failed\[:.\] \(\[^\\r\]*\)\\r" {
			fatal 4 "Failed: \$expect_out(1,string)\n"
		}
	    -re "Failed\[:.\]\\r" {
			fatal 4 "Failed\n"
		}
	    -re "Success\[:.\] \(\[^\\r\]*\)\\r" {
			tell_user "Success: \$expect_out(1,string)\n"
		}
	    -re "Success\[:.\]\\r" {
			tell_user "Success\n"
		}
	    "\$prompt > " {
			tell_user "Failed: Unexpected command prompt\n"
		}
	    "Diag > " {
			tell_user "Failed: Unexpected diagnostic prompt\n"
		}
	    "Factory > " {
			tell_user "Failed: Unexpected factory prompt\n"
		}
	    "Ignored. Unknown command" {
			fatal 4 "Unknown command"
		}
	    timeout {
			fatal 3 "Timeout"
		}
	    }
	}

	# Reboot the firmware
	# Need to get to the bootstrap for a firmware upload.
	#
	proc fw_reboot {} {
	    set timeout 5

	    send -- "\003"
	    sleep 0.1
	    send -- "\003"

	    expect {
		"SoH > "     { send "diag\r" ; exp_continue }
		"Lev > "     { send "diag\r" ; exp_continue }
		"Diag > "    { send "reset\r" }
		"Factory > " { send "reset\r" }
		timeout {
		    if { "$mode_silent" == "true" } {
			fatal 3 "Cannot reset target"
		    }
		    # Request the user press reset to start the sequence
		    set timeout 30
		    send_user "Reset target to start upgrade\n"
		}
	    }

	    # Boot banner
	    expect {
		"SoH Boot " { }
		"Leveller Boot " { }
		timeout {
		    fatal 3 "No boot string seen"
		}
	    }

	    # Send the wakeup message
	    send -- "wakeup"

	    # Boot prompt, initiate Load
	    expect {
		"Boot ? " { }
		timeout {
		    fatal 3 "Failed to enter bootstrap"
		}
	    }
	}

	# Firmware upload sequence
	#
	proc upload_sequence {} {
	    fw_reboot

	    send -- "L"

	    expect {
		"Start file transfer" { }
		timeout {
		    fatal 3 "Failed to start transfer"
		}
	    }

	    # Send the file
	    send_hex_file "$hexfile"

	    # Boot prompt again, this time start the new firmware
	    expect {
		"Boot ? " { }
		timeout {
		    fatal 3 "Missing post upgrade prompt"
		}
	    }

	    send -- "G"

	    # Wait for the firmware prompt and report mode
	    set timeout 10
	    expect {
		"Lev > " {
		    tell_user "Leveller firmware in normal mode\n"
		}
		"SoH > " {
		    tell_user "SoH firmware in normal mode\n"
		}
		"Factory > " {
		    tell_user "Firmware in FACTORY mode\n"
		}
		timeout {
		    fatal 3 "Firmware failed to start"
		}
	    }
	}


	# Leveller actions
	#
	proc level_action { act } {
	    issue_command "Lev" "true" "\$act"
	    wait_for_completion "Lev"
	}


	# SoH actions
	#
	proc usoh_action { act } {
	    issue_command "SoH" "true" "\$act"
	    wait_for_completion "SoH"
	}


	# Main expect script starts here

	set timeout 3
	if { "$mode_debug" == "true" } {
	    log_user 1
	} else {
	    log_user 0
	}

	# Use picocom via openport to connect
	spawn -ignore SIGINT -ignore SIGTERM -ignore SIGHUP \
		openport -p $port_name
	expect {
	    "Terminal ready\r" {
		sleep 0.01
	    }
	    "FATAL: cannot lock" {
		fatal 3 "Port $port_name already locked"
	    }
	    timeout {
		fatal 3 "Port $port_name failed to start picocom"
	    }
	}

	switch "$action" {
	    upload {
		upload_sequence
	    }
	    level -
	    hunt -
	    datum {
		level_action "$action"
	    }
	    lock -
	    unlock -
	    center {
		usoh_action "$action"
	    }
	    default {
		fatal 3 "Action $action not implemented"
	    }
	}

	exit 0
EOF
    rc=$?
    $mode_debug && echo "Action script returns $rc"
else
    $mode_debug && echo "Action script not run. act=$action port=$port_name"
    rc=2
fi


# Attempt to restore previous power state
if [ -n "$power2_name" -a "$power2_before" == "0" ]
then
    ioline --line "$power2_name" --q-output 0
    if [ $? != 0 ]
    then
	$mode_silent || echo "Failed to turn $power2_name off" 1>&2
    fi
fi
if [ -n "$power1_name" -a "$power1_before" == "0" ]
then
    ioline --line "$power1_name" --q-output 0
    if [ $? != 0 ]
    then
	$mode_silent || echo "Failed to turn $power1_name off" 1>&2
    fi
fi

exit $rc
