#!/bin/bash
###############################################################################
# $Id: gdsh.functions.sh.in,v 1.62 2010/12/31 02:38:02 tw Exp $
# 
# Shell functions for gdsh
#
# Copyright (C) 2008 - 2010 Thomas Weidenfeller
# 
# This file is part of gds2000tools/gdsh.
# 
# gds2000tools/gdsh is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 3 as
# published by the Free Software Foundation.
# 
# gds2000tools/gdsh is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
# 
# You should have received a copy of the GNU General Public License
# along with gds2000tools/gdsh.  If not, see <http://www.gnu.org/licenses/>.
###############################################################################


#
# TODO: use alarm to create timeout if echo during sending hangs?
#

###############################################################################
# Low level, debug and internal functions
###############################################################################

function _gdsh_dbg () {
	echo "gdsh debug: $@" >&2
}

GDSH_DMSG=':'
[ "0$GDSH_DEBUG" -gt 0 ] && GDSH_DMSG='_gdsh_dbg'
[ "0$GDSH_DEBUG" -gt 1 ] && set -x

$GDSH_DMSG "Loading lib/gdsh.functions ..."


function _gds-send () {
	$GDSH_DMSG "_gds-send $@ (fd: $GDSH_FD)"
	[ "$GDSH_FD" -lt "$GDSH_FD_MIN" -o "$GDSH_FD" -gt "$GDSH_FD_MAX" ] && return 1
	echo "$@" >&$GDSH_FD
}

function _gds-read () {
	$GDSH_DMSG "_gds-read $@ (fd: $GDSH_FD)"
	[ "$GDSH_FD" -lt "$GDSH_FD_MIN" -o "$GDSH_FD" -gt "$GDSH_FD_MAX" ] && return 1
	read -t ${GDSH_READ_TIMEOUT:-3} -u $GDSH_FD GDSH_RESP
}

function _gdsh_get_unused_fd () {
	$GDSH_DMSG "_gdsh_get_unused_fd $@"
	for fd in $(seq $GDSH_FD_MIN $GDSH_FD_MAX) ; do
		[ -z "${GDSH_FDS[$fd]}" ] && { echo $fd; return 0; }
	done
	return 1;
}

# print an information message if shell is interactive
function _gdsh_imsg () {
	if [ -n "$PS1" ] ; then
		name="$1"
		shift
		echo "[$name: $@]" >&2
	fi
}


###############################################################################
# Public commands and helper functions for them
# See "man gdsh" for documentation
###############################################################################

function gds-avail () {
	$GDSH_DMSG "gds-avail $@"

	ls -1 /dev/GDS* /dev/*ACM* /dev/*USB* /dev/ttyS* 2>/dev/null
}



function _gds-open-usage () {
	cat >&2 <<!
usage gds-open [-h] [[-u] <usb-device>]
      gds-open [-h] [[-s] <rs232-device> [<baud>] [<parity>] [<stopbits>]]
-h This help text
-u Force to use USB communication defaults
-s Force to use RS232 communication defaults if non provided on the
   command line.
!
}

function gds-open () {
	$GDSH_DMSG "gds-open $@"
	local type="${GDSH_DEV_TYPE:-?}"
	local o
	OPTIND=1
	while getopts "hsu" o
	do
		case "$o" in
		h)	_gds-open-usage 2>&1
			return 0
			;;
		s)	type="s"	# force serial defaults
			;;
		u)	type="u"	# force USB defaults
			;;
		*)
			_gds-open-usage
			return 1
			;;
		esac
	done
	shift $(($OPTIND - 1))
	local dev="${1:-${GDSH_DEV:?'gds-open: no device specified'}}" || return 1

	#
	# TODO: For USB: Select hardware handshake
	#
	#[ $# -gt 1 ] && type="s"	# force serial parameters if some
					# are given on the command line
	if [ "$type" = '?' ] ; then
		type="s"
		case "$dev" in
		*ACM*|*USB*|*ttyU*|*/GDS*)
			$GDSH_DMSG "gds-open: assuming USB"
			type="u"
			;;
		esac
	fi
	local baud
	local parity
	local stopbits
	case "$type" in
	s)
		baud=${2:-${GDSH_DEV_BAUD:-19200}}     # RS232 baud
		parity=${3:-${GDSH_DEV_PARITY:-N}}     # RS232 parity
		stopbits=${4:-${GDSH_DEV_STOPBITS:-1}} # RS232 stop bits
		;;
	u)
		# pseudo parameters for USB
		[ $# -gt 1 ] && echo "gds-open: Warning: USB device, ignoring serial parameter(s) $2 $3 $4" >&2
		baud=1152000 	# some drivers understand larger values, e.g.
				# up to 460800, but 1152000 should be enough
		parity=N
		stopbits=1
		;;
	*)	echo "gds-open: unknown device type $type set in \$GDSH_DEV_TYPE (only u, s, or ? are allowed)" >&2
		return 2
		;;
	esac
	$GDSH_DMSG "gds-open: device $dev"
	$GDSH_DMSG "gds-open: baud $baud"
	$GDSH_DMSG "gds-open: parity $parity"
	$GDSH_DMSG "gds-open: stopbits $stopbits"

	local pArg="-parenb -ignpar -inpck" # no parity
	case "$parity" in 
	n|N)	 
		;;
	e|E)	pArg="parenb -parodd ignpar inpck"
		;;
	o|O)	pArg="parenb parodd ignpar inpck"
		;;
	*)	_gds-open-usage
		echo "gds-open: unsupported parity '$parity' (only n, N, e, E, o, O are supported)" >&2
		return 1
		;;
	esac

	local sArg="-cstopb"	# 1 stopbit
	case "$stopbits" in 
	1)	
		;;
	2)	sArg="cstopb"
		;;
	*)	_gds-open-usage
		echo "gds-open: unsupported number of stop bits (only 1 and 2 are supported)" >&2
		return 1
		;;
	esac

	#
	# Try to get a file descriptor to be used for permanently
	# keeping the device to the instrument open
	#
	local newfd=$(_gdsh_get_unused_fd)
	if [ -z "$newfd" ] ; then
		echo "gds-open: all file descriptors in use" >&2
		return 1
	fi
	$GDSH_DMSG "gds-open: newfd: $newfd"

	#
	# Avoid problems with expansion in exec redirection
	# Some (all) bash don't like
	# exec $newfd<>"$dev" || {
	#	echo "gds-open: failed to open $dev as $newfd" >&2; return 1; }
	#
	local r=0
	case $newfd in
	3)	exec 3<>"$dev"; r=$? ; $GDSH_DMSG "gds-open exec 3" ;;
	4)	exec 4<>"$dev"; r=$? ; $GDSH_DMSG "gds-open exec 4" ;;
	5)	exec 5<>"$dev"; r=$? ; $GDSH_DMSG "gds-open exec 5" ;;
	6)	exec 6<>"$dev"; r=$? ; $GDSH_DMSG "gds-open exec 6" ;;
	7)	exec 7<>"$dev"; r=$? ; $GDSH_DMSG "gds-open exec 7" ;;
	8)	exec 8<>"$dev"; r=$? ; $GDSH_DMSG "gds-open exec 8" ;;
	9)	exec 9<>"$dev"; r=$? ; $GDSH_DMSG "gds-open exec 9" ;;
	*)	echo "gds-open: Internal error."  >&2
		return 1
		;;
	esac
	if [ $r -ne 0 ] ; then
		_gdsh_imsg gds-open "Failed to open '$dev' on file descriptor $newfd ($r)]"
		return $r
	fi

	GDSH_FDS[$newfd]="$dev"
	gds-use $newfd

	# TODO: Sort out unneeded options
	stty	\
		$baud		\
		clocal		\
		cread		\
		-crtscts	\
		cs8		\
		$sArg		\
		$pArg		\
		-brkint		\
		-icrnl		\
		ignbrk		\
		-igncr		\
		-inlcr		\
		-istrip		\
		-iuclc		\
		-ixoff		\
		-ixon		\
		-parmrk		\
		ocrnl		\
		-ofdel		\
		-ofill		\
		-olcuc		\
		-onlcr		\
		-onlret		\
		-onocr		\
		-opost		\
		-echo		\
		<&$newfd
	r=$?
	$GDSH_DMSG "gds-open: stty on $newfd: "
	[ "0$GDSH_DEBUG" -gt 0 ] && stty -a <&$newfd >&2
	[ $r -ne 0 ] && gds-close $newfd
	if [ -n "$PS1" ] ; then
		if [ $r -eq 0 ] ; then
			_gdsh_imsg gds-open "$dev $baud $parity $stopbits configured on fd $newfd"
			local idn
			idn=`_idn_`
			if [ $? -eq 0 ] ; then
				_gdsh_imsg gds-open "IDN: $idn"
			else
				_gdsh_imsg gds-open "*IDN? failed, no oscilloscope connected?"
			fi
		else
			_gdsh_imsg gds-open "$dev $baud $parity $stopbits nok"
		fi
	else
		#
		# Not interactive, return FD if valid
		#
		if [ $r -eq 0 ] ; then
			echo $newfd
		fi
	fi
	return $r
}

function gds-use () {
	$GDSH_DMSG "gds-use $@"
	case $# in
	0)
		if [ "$GDSH_FD" -lt 0 ] ; then
			echo "$GDSH_FD	no device open"
		else
			local fd
			for fd in $(seq $GDSH_FD_MIN $GDSH_FD_MAX) ; do
				if [ -n "${GDSH_FDS[$fd]}" ] ; then
					if [ $fd -eq "$GDSH_FD" ] ; then
						echo "*$fd	${GDSH_FDS[$fd]}"
					else
						echo " $fd	${GDSH_FDS[$fd]}"
					fi
				fi
			done
		fi
		return 0
		;;
	1)	
		if expr "$1" : "[x 0-9][0-9]*$" >/dev/null ; then
			if [ -n "${GDSH_FDS[$1]}" ] ; then
				GDSH_FD="$1"
				return 0
			else
				echo "gds-use: File descriptor $1 not open" >&2
				return 1
			fi
		else
			echo "gds-use: Illegal file descriptor $1" >&2
			return 1
		fi
		;;
	*)	echo "usage: gds-use [<fd>]" >&2
		return 1
		;;
	esac
	return 1
}


# TODO: Find a way to suppress bash's "bad file descriptor" message
#

function gds-is-open () {
	$GDSH_DMSG "gds-is-open $@"
	local fd
	case $# in
	0)	fd="$GDSH_FD"
		;;
	1)	fd="$1"
		;;
	*)	echo "usage: gds-is-open [<fd>]" >&2
		return 1
		;;
	esac
	if [ "$fd" -eq -1 ] ; then
		_gdsh_imsg gds-is-open "Oscilloscope with fd $fd not open"
		return 1
	fi

	local r=0
	if [ "$fd" -lt "$GDSH_FD_MIN" -o "$fd" -gt "$GDSH_FD_MAX" ] ; then
		echo "gds-is-open: Illegal file descriptor $fd" >&2
		return 1
	fi
	[ -z "${GDSH_FDS[$fd]}" ] && r=1
	if [ $r -eq 0 ] ; then
		: echo >&$fd
		r=$?
	fi
	if [ $r -eq 0 ] ; then
		_gdsh_imsg gds-is-open "Oscilloscope with fd $fd ${GDSH_FDS[$fd]} open"
	else
		_gdsh_imsg gds-is-open "Oscilloscope with fd $fd ${GDSH_FDS[$fd]} not open"
	fi
	return $r
}


function gds-close () {
	$GDSH_DMSG "gds-close $@"
	local fd
	case $# in
	0)	fd="$GDSH_FD"
		;;
	1)	fd="$1"
		;;
	*)	echo "usage: gds-close [<fd>]" >&2
		return 1
		;;
	esac


	#
	# Using a shell variable here crashes some bash versions if
	# the file descriptor does not exist
	#
	# exec $fd<&-
	# exec $fd>&-
	#
	local r=0
	case "$fd" in
	-1)	_gdsh_imsg gds-close "Oscilloscope with fd $fd not open"
		return 0
		;;
	3)	exec 3<&-
		exec 3>&-
		r=$?
		;;
	4)	exec 4<&-
		exec 4>&-
		r=$?
		;;
	5)	exec 5<&-
		exec 5>&-
		r=$?
		;;
	6)	exec 6<&-
		exec 6>&-
		r=$?
		;;
	7)	exec 7<&-
		exec 7>&-
		r=$?
		;;
	8)	exec 8<&-
		exec 8>&-
		r=$?
		;;
	9)	exec 9<&-
		exec 9>&-
		r=$?
		;;
	*)	echo "gds-close: Illegal file descriptor $fd" >&2
		[ "$fd" -eq "$GDSH_FD" ] && GDSH_FD=-1
		return 1
		;;
	esac
	[ $r -ne 0 ] && _gdsh_imsg gds-close "Failed to close file descriptor $fd"
	if [ -z ${GDSH_FDS[$fd]} ] ; then
		r=2
		_gdsh_imsg gds-close "File descriptor $fd was not in use"
	fi
	GDSH_FDS[$fd]=""
	[ "$fd" -eq "$GDSH_FD" ] && GDSH_FD=-1
	return $r
}

function gds-flush () {
	$GDSH_DMSG "gds-flush $@"
	local fd
	case $# in
	0)	fd="$GDSH_FD"
		;;
	1)	fd="$1"
		;;
	*)	echo "usage: gds-flush [<fd>]" >&2
		return 1
		;;
	esac
	if [ "$fd" -lt "$GDSH_FD_MIN" -o "$fd" -gt "$GDSH_FD_MAX" ] ; then
		echo "gds-flush: Illegal file descriptor $fd" >&2
		return 1
	fi

	local dummy
	while read -t 1 -u $fd dummy
	do
		:
	done
}

function gds-read-silent () {
	$GDSH_DMSG "gds-read-silent $@"
	_gds-read
	local r=$?
	if [ $r -eq 0 ] ; then
		_gdsh_imsg gds-read-silent 'OK, result in $GDSH_RESP'
	else
		_gdsh_imsg gds-read-silent "not OK"
	fi
	return $r
}

function gds-read () {
	$GDSH_DMSG "gds-read $@"
	_gds-read
	local r=$?
	if [ $r -eq 0 ] ; then
		echo "$GDSH_RESP"
	else
		_gdsh_imsg gds-read "not OK"
	fi
	return $r
}

function gds-query-silent () {
	$GDSH_DMSG "gds-query-silent $@"
	_gds-send "$@" || return 1
	gds-read-silent
}

function gds-query () {
	$GDSH_DMSG "gds-query $@"
	_gds-send "$@" || return 1
	gds-read
}


function gds-query-bin () {
	$GDSH_DMSG "gds-query-bin $@"
	if [ "$1" = '-f' ] ; then
		# need to reorder arguments
		local a1="$1"; shift
		local a2="$1"; shift
		local a3="$1"; shift
		set -- "$a3" "$a1" "$a2" "$@"
	fi
	_gds-send "$1" || return 1
	shift
	$GDSH_BINDIR/gds-read-bin -t ${GDSH_READ_TIMEOUT:-3} "$@" <&$GDSH_FD || { gds-flush; return 1; }
}

function gds-send () {
	$GDSH_DMSG "gds-send $@"
	local cmd="$1"; shift
	local arg="$1"; shift
	[ "$arg" = "_" ] && arg='?'
	if expr "$cmd" : '.*[_?]$' > /dev/null || [ "$arg" = '?' ] ; then
		gds-query "$cmd" "$arg" "$@"
	else
		_gds-send "$cmd" "$arg" "$@"
	fi
}

function gds-send-bin () {
	$GDSH_DMSG "gds-send-bin $@"
	local cmd="$1"; shift
	local arg="$2"; shift
	[ "$arg" = "_" ] && arg='?'
	if expr "$cmd" : '.*[_?]$' > /dev/null || [ "$arg" = '?' ] ; then
		gds-query-bin "$cmd" "$arg" "$@"
	else
		echo "$cmd" "$arg" "$@" >&9
	fi
}

###############################################################################
# High-level functions
# See "man gdsh" for documentation
###############################################################################


function _gds-measure-usage () {
	cat >&2 <<!
usage: gds-measure [-h] [-f] [-p <timestamp-format>] [-s <sec>] [-F <fieldsep>] [-L <lines>] [-0]  (<ch> (<measure>...))...
   -h         This help text
   -f         Continuously measure. Displays a column header, too.
   -p <timestamp-format>
              Format for the 'ts' timestamp. See date(1) for format information.
   -s <sec>   Delay after each round of measures (if -f is used)
   -F <fieldsep>
              Output field separator. Default: space.
   -L <lines> After how many lines to insert -f column header again.
              Default: Number of terminal lines -2.
	      If 0: headers aren't repeated.
   -0         In continuous mode (-f): No headers are printed at all
              In single measurement mode: No value names and units are printed.
   <ch>	      Separate measurement-channel specification 1 ... 4
   <measure>  Measurement specifications:
      ts|T          Display a timestamp. See Also -p option.
      n|N	    Display current measurement interval number (for -f)
      tfreq         Trigger-frequency measurement
      <type>[<ch>]  Per-channel measurement. <type>s are:
                    Frequency: freq
                    Voltages:  vamp vav vhi vlo vmax vmin vpp vrms
                    Ratios:    fovs frp pdut rovs rpr
                    Timing:    fall nwid per pwid ris
      D<s> 1|2|3|4  Delay measurement source channel specification
      <dtype>       Delay-between-channel measurement. <dtype>s are:
                    fffd ffrd frfd frrd lffd lfrd lrfd lrrd
Examples:
   gds-measure -f n tfreq D1 1 D2 2 frfd
   # The following examples all provide the same output
   gds-measure vmin1 vmax1 vmin2 vmax2
   gds-measure 1 vmin vmax 2 vmin vmax
   gds-measure vmin1 vmax vmin2 vmax
!
}

function gds-measure () {
	$GDSH_DMSG "gds-measure $@"
	local o
	local contflag=0
	local slptime=0
	local headerflag=1
	local ch='1'
	local tsf="%s"	# default timestamp format: seconds since epoch
	local fieldsep=" "
	local lines=$((${LINES:--2} - 2))
	
	OPTIND=1
	while getopts "hfs:p:F:L:0" o
	do
		case "$o" in
		h)
			_gds-measure-usage 2>&1
			return 0
			;;
		f)	contflag=1
			[ "$slptime" -eq 0 ] && slptime=1
			;;
		s)	slptime="$OPTARG"
			;;
		p)	tsf="$OPTARG"
			;;
		F)	fieldsep="$OPTARG"
			;;
		L)	lines="$OPTARG"
			;;
		0)	headerflag=0
			;;
		*)
			_gds-measure-usage
			return 2
			;;
		esac
	done
	shift $(($OPTIND - 1))
	[ $# -lt 1 ] && { _gds-measure-usage; return 1; }


	local cmds=""	# command sequence to be build
	local hdr="#"	# output header, identifying data positions

	local ch=0	# current channel number
	local -a dchs=([1]=0 0)	# current delay source channels
	local cfs=""	# current field separator
	while [ $# -gt 0 ] ; do
		local meas	# current measurement name
		local cmd	# current command to be added
		local unit	# current unit for measurement result

		case "$1" in
		D1|D2)	# delay channel source spec
			[ $# -le 1 ] && { echo "gds-measure: Error: Channel number missing after $1" >&2; _gds-measure-usage; return 1; }
			local dm=${1:1:1}
			shift
			case "$1" in
			1|2|3|4) ;;
			*) 	 echo "gds-measure: Error: Channel number D$dm $1 invalid1" >&2
				_gds-measure-usage
				return 1
				;; 
			esac
			dchs[$dm]=$1
			cmds="${cmds}:meas:delay$dm $1;"
			shift
			continue	# !
			;;
		*1|*2|*3|*4)	# channel spec, optional with a measurement spec
			local nch=${1:$((${#1} - 1))}
			[ "$ch" != "$nch" ] && cmds="${cmds}:meas:sour $nch;"
			ch=$nch
			# process rest of argument, if any
			local again=${1:0:$((${#1} - 1))}
			shift
			[ -n "$again" ] && set -- "$again" "$@"
			continue	# !
			;;
		n|N)	# Current measurement interval
			meas="N"
			cmd="echo \${n}"
			unit=""
			;;
		ts|T)	# Timestamp
			meas="T"
			cmd="date +${tsf}"
			unit=""	# TODO: parse tsf to figure out unit
			;;
		tfreq|tf|ft)	# Trigger frequency
			meas="ft"
			cmd=":trig:freq_"
			unit="Hz"
			;;
		fffd|ffrd|frfd|frrd|lffd|lfrd|lrfd|lrrd)
			# Delays between channels
			if [ ${dchs[1]} -lt 1 -o ${dchs[2]} -lt 1 ] ; then
				echo "gds-measure: Error: Source channel(s) (D<s>) not specified for $1" >&2
				_gds-measure-usage
				return 1
			fi
			meas="T${1}(${dchs[1]},${dchs[2]})"
			cmd=":meas:${1}_"
			unit="s"
			;;
		freq|f)
			# Per-channel frequency
			[ $ch -lt 1 -o $ch -gt 4 ] && { _gds-measure-usage; return 1; }
			meas="f${ch}"
			cmd=":meas:freq_"
			unit="Hz"
			;;
		fall|nwid|per|pwid|ris)
			# Timing
			[ $ch -lt 1 -o $ch -gt 4 ] && { _gds-measure-usage; return 1; }
			meas="T${1}${ch}"
			cmd=":meas:${1}_"
			unit="s"
			;;
		fovs|frp|pdut|rovs|rpr)
			# Ratios
			[ $ch -lt 1 -o $ch -gt 4 ] && { _gds-measure-usage; return 1; }
			meas="R${1}${ch}"
			cmd=":meas:${1}_"
			unit=""
			;;
		vamp|vav|vhi|vlo|vmax|vmin|vpp|vrms)
			# Voltages
			[ $ch -lt 1 -o $ch -gt 4 ] && { _gds-measure-usage; return 1; }
			meas="U${1:1}${ch}"
			cmd=":meas:${1}_"
			unit="V"
			;;
		*)
			_gds-measure-usage
			return 1
			;;
		esac
		shift
		if [  $contflag -ne 0 ] ; then
			# Data format to be used in continuous mode
			cmds="${cmds}echo -n \"$cfs\$($cmd || return 1;)\";"
			if [ -z "$unit" ] ; then
				hdr="${hdr}$cfs${meas}"
			else
				hdr="${hdr}$cfs${meas}/$unit"
			fi
		else
			# Data format to be used in single measurement mode
			if [ $headerflag -ne 0 ] ; then
				cmds="${cmds}echo -n \"$cfs${meas}=\$($cmd)$unit\";"
			else
				cmds="${cmds}echo -n \"$cfs\$($cmd)\";"
			fi
		fi
		cfs="$fieldsep"
	done
	cmds="${cmds}echo;"	# each measurement interval on a single line
	[ $slptime -ne 0 ]   && cmds="${cmds}sleep $slptime;"
	if [ $contflag -ne 0 ] ; then
		cmds="while :;do ${cmds}"
		if [ $headerflag -ne 0 ] ; then
			cmds="echo \"$hdr\";${cmds}"
			if [ $lines -gt 0 ] ; then
				cmds="${cmds}[ \$((\$n % \$lines)) -eq 0 ] && echo \"$hdr\";n=\$((\$n + 1));"
			fi
		fi
		cmds="${cmds}done;"
	fi
	cmds="local n=1;${cmds}"
	$GDSH_DMSG "$cmds"
	( eval "$cmds" )  # subshell so Ctrl-C doesn't skip following gds-flush
	gds-flush
}


function gds-sync-time () {
	$GDSH_DMSG "gds-sync-time $@"
	:syst:tim `date +"%H %M %S"` || return 1
	_gdsh_imsg gds-sync-time "Oscilloscope can take up to a minute to display new time"
}

function gds-sync-date () {
	$GDSH_DMSG "gds-sync-date $@"
	:syst:dat `date +"%d %m %Y"` || return 1
	_gdsh_imsg gds-sync-date "Oscilloscope can take up to a minute to display new date"
}

function gds-sync () {
	$GDSH_DMSG "gds-sync $@"
	gds-sync-date
	gds-sync-time 
}

function gds-template-get () {
	$GDSH_DMSG "gds-template-get $@"
	[ $# -ne 1 ] && { echo "usage: gds-template-get <nbr>" >&2; return 1; }
	local old=$(:gon:func_)
	:gon:func 1
	gds-query-bin -f go-nogo "TEMPlate${1}:DOWNload?"	
	local res=$?
	:gon:func $old
	return $res
}
function gds-template-download () {
	gds-template-get "$@"
}


function  _gds-csv-usage () {
cat >&2 <<!
usage: gds-csv [-h] [-b] [-F <fs>] [-l] channel-number
Returns current measurement data as comma separated value list.
-h	This help information
-b	Mimic GDS-2000 v1.04 firmware bugs.
-l	Read the 'long memory' instead of the normal memory
	(GDS-1000A only)
-F <fs>	Field separator, default ','.
!
}

function gds-csv () {
	$GDSH_DMSG "gds-cvs $@"

	#
	# Bug flags. Mimic known bugs of the GDS-2000 v1.04 or v1.09
	# firmware if 1
	#
	local bug_trailing=0	# additional trailing separator v1.04, 1.09
	local bug_spelling=0	# misspelling of "firmware" as "firmwave" v1.04

	#
	# Default field separator
	#
	local fieldsep=","

	local acq_cmd=':acquire${channel}:memory_'

	local o
	OPTIND=1
	while getopts "bF:hl" o
	do
		case "$o" in
		b)	bug_trailing=1
			bug_spelling=1
			;;
		F)	fieldsep="$OPTARG"
			;;
		h)	_gds-csv-usage 2>&1
			return 0
			;;
		l)	if gds-is-1000A
			then
				acq_cmd=':acquire${channel}:lmemory_'
			else
				echo "gds-csv: -l option not supported for "`gds-model` >&2
				return 1
			fi
			;;
		*)	 _gds-csv-usage
			return 1
			;;
		esac
	done
	shift $(($OPTIND - 1))
	[ $# -ne 1 ] && {  _gds-csv-usage; return 1; }
	local channel="$1"
	case "$channel" in
	1|2|3|4) ;;
	*)	 _gds-csv-usage
		return 1
		;;
	esac

	if [ $bug_trailing -eq 0 ] ; then
		lastsep=""
	else
		lastsep="$fieldsep"
	fi

	#
	# Keep order of entries as similar as possible to the order
	# the oscilloscope uses for .csv files
	#
	# + Memory Length
	# - Trigger Level
	# + Source
	# - [Probe] Not supported, GDS-1000A might have this entry here
	# - Vertical Units
	# - Vertical Scale
	# - Vertical Position
	# - Horizontal Units
	# - Horizontal Scale
	# - Horizontal Position
	# - Horizontal Mode
	# - [Display Last Dot Address]  Only sometimes written, anyhow not
	# 			      	really supported
	# - [Waveform Last Dot Address] Only sometimes written, anyhow not
	#			      	really supported
	# * Sampling Period
	# - Firmware
	# - Time
	# - [Mode] Not supported, GDS-1000A might have this entry here
	# + Waveform Data
	#
	#
	#

	local block1
	block1="Trigger Level$fieldsep"`:trig:lev_`"$lastsep" || return 1

	local block2="Vertical Units${fieldsep}V$lastsep"
	block2="${block2}\nVertical Scale${fieldsep}"`:chan${channel}:scal_`"$lastsep" || return 1
	block2="${block2}\nVertical Position$fieldsep"`:chan${channel}:offs_`"$lastsep" || return 1

	#
	# Horizontal units
	#
	local timswep=`:tim:swe_` || return 1
	local hscale
	local hunit
	local hmode
	local hpos
	case "$timswep" in
	0)	hmode="Main"
		hunit="s"
		hscale=`:tim:scal_` || return 1
		hpos=`:tim:del_` || return 1
		;;

	1)	hmode="Window" # TODO: Save/recall returns this as "Window Zoom"
		hunit="s"
		#
		# GDS-2000 v1.09 firmware uses window zoom delay here, not
		# normal delay
		#
		hscale=`:tim:scal_` || return 1
		hpos=`:tim:wind:del_` || return 1
		;;

	2)	hmode="Window Zoom"
		hunit="s"
		#
		# Need to use the window scale, not the normal scale
		# for zoomed datas. Same for the hoirizontal position.
		#
		hscale=`:tim:wind:scal_` || return 1
		hpos=`:tim:wind:del_` || return 1
		;;

	3)	#
		# HACK: Scope firmware does not distinguish between Scan mode
		#       and Roll mode and returns Scan mode as Roll mode, too.
		#
		#	Roll mode requires a horizontal scale >= 250ms
		#	Everything below must be Scan mode. However,
		#	everything above can still be Scan or Roll mode.
		#	Hence the warning.
		#
		hscale=`:tim:scal_` || return 1
		gawk -v "hscale=$hscale" 'BEGIN { exit hscale > 0.250 }'
		if [ $? -eq 0 ] ; then
			hmode="Roll Mode"
			echo "gds-csv: warning: Can't detect if in Scan or Roll Mode. Assuming $hmode." >&2
		else
			hmode="Scan Mode"
		fi
		hunit="s"
		hpos=`:tim:wind:del_` || return 1
		;;

	4)	hmode="XY Mode"
		hunit="V"	# TODO: Save/recall uses "s" here, too.
		#
		# :tim:del_ fails when in XY mode (at least on the GDS-2000)
		# Oscilloscope stores a 0 in files here
		#
		hscale=`:tim:scal_` || return 1
		hpos="0"
		;;

	?)	hmode="?"
		hunit="s"	# Likely it is Seconds
		hscale=`:tim:scal_` || return 1
		hpos=`:tim:del_` || return 1
		;;
	esac
	block2="${block2}\nHorizontal Units${fieldsep}${hunit}$lastsep"
	block2="${block2}\nHorizontal Scale${fieldsep}${hscale}$lastsep"


	block2="${block2}\nHorizontal Position${fieldsep}${hpos}$lastsep"
	block2="${block2}\nHorizontal Mode${fieldsep}${hmode}$lastsep"
	if [ "$timswep" -eq 3 ] ; then
		#
		# In scan mode / ROLL mode.
		#
		# TODO: How to obtain these values?
		#
		echo "gds-csv: warning: $hmode: Can't query Display Last Dot Address." >&2
		block2="${block2}\nDisplay Last Dot Address${fieldsep}?${lastsep}"
		echo "gds-csv: warning: $hmode: Can't query Waveform Last Dot Address." >&2
		block2="${block2}\nWaveform Last Dot Address${fieldsep}?${lastsep}"
	fi

	#
	# Can't trust Sampling Period from acquired data. GDS-2000 v1.09
	# firmware (and maybe others) deliver wrong Sampling Period data
	# when in window zoom mode ...
	# Instead calculate it from horizontal scale.
	#
	#local sp=`awk -v ts=$hscale 'BEGIN { printf "%.3E\n",ts/25 }'`
	#block2="${block2}\nSampling Period${fieldsep}${sp}$lastsep"

	local block3

	if [ $bug_spelling -eq 0 ] ; then
		block3="Firmware$fieldsep"`gds-version`"$lastsep" || return 1
	else
		# Original spelling error "wave" instead of "ware" included
		block3="Firmwave$fieldsep"`gds-version`"$lastsep" || return 1
	fi

	block3="${block3}\n"`env LC_TIME=C date "+Time${fieldsep}%d-%b-%y %R${lastsep}" `


	eval $acq_cmd -F "\"$fieldsep\"" -1 -i | gawk \
		-v "block1=$block1" -v "block2=$block2" -v "block3=$block3" \
		-v "lastsep=$lastsep" '
		f { print $0 lastsep; next }
		/^Memory Length/   { ml=$0;  next }
		/^Source/          { src=$0; next }
		/^Sampling Period/ { sp=$0;  next }
		/^Waveform Data/ {
			print ml lastsep
			print block1
			print src lastsep
			print block2
			print sp lastsep
			print block3
			print $0 lastsep
			f=1
		}'
}

function gds-manufacturer () {
	local id
	id=`_idn_` || return 1
	local oldifs="$IFS"
	IFS=",$IFS"
	set -- $id
	IFS="$oldifs"
	echo "$1"
}

function gds-vendor () {
	gds-manufacturer
}


function gds-model () {
	local id
	id=`_idn_` || return 1
	local oldifs="$IFS"
	IFS=",$IFS"
	set -- $id
	IFS="$oldifs"
	echo "$2"
}

function gds-type () {
	gds-model
}

function gds-serialnum () {
	local id
	id=`_idn_` || return 1
	local oldifs="$IFS"
	IFS=",$IFS"
	set -- $id
	IFS="$oldifs"
	echo "$3"
}


function gds-version () {
	local id
	id=`_idn_` || return 1
	local oldifs="$IFS"
	IFS=",$IFS"
	set -- $id
	IFS="$oldifs"
	echo "$4"
}

function gds-firmware() {
	gds-version
}


function  _gds-channels-usage () {
cat >&2 <<-!
	usage: gds-channels [-h|-a|-e]
	Number of available oscilloscope channels.
	-h	This help information
	-a	Returns active instead of available channels.
	-e	Returns a list of channel numbers instead of the number of channels.
!
}

function gds-channels () {
	local o
	local a=0
	local e=0
	OPTIND=1
	while getopts "aeh" o
	do
		case "$o" in
		a)	a=1 ;;
		e)	e=1 ;;
		h)	_gds-channels-usage 2>&1
			return 0
			;;
		*)	 _gds-channels-usage
			return 1
			;;
		esac
	done
	[ $a -ne 0 -a $e -ne 0 ] && {  _gds-channels-usage; return 1; }
	local t
	t=`gds-type` || return 1
	local tot=$(expr "$t" : '.*\(.\)$')
	[ "0$tot" -lt 1 ] && tot=2	# hack for GDS-8x0
	if [ $e -ne 0 ] ; then
		local l=""
		local s=""
		while [ "0$tot" -gt 0 ] ; do
			l="$tot$s$l"
			s=" "
			tot=$(($tot - 1))
		done
		echo $l;
	elif [ $a -ne 0 ] ; then
		local l=""
		local s=""
		while [ "0$tot" -gt 0 ] ; do
			if [ 0`:chan${tot}:disp_` -ne 0 ] ; then
				l="$tot$s$l"
				s=" "
			fi
			tot=$(($tot - 1))
		done
		echo $l
	else
		echo $tot
	fi
}


function gds-bandwidth () {
	echo $(($(expr $(gds-type) : '.*\(..\).$') * 10))
}

function gds-is-1000A () {
	expr $(gds-model) : '.*A$' > /dev/null
}



#
function _gds-snapshot-usage () {
cat  >&2 <<!
usage: gds-snapshot [-h] [-c <colormap>] [-n] [--] [<image.bmp>]
Get a screen snapshot as BMP file.
-h	This help information
-c <colormap>
	Which colormap should be used to create the image.

	standard	Standard (dark) colormap.
	improved	Slightly lighter colormap.
	inksave		Inksave (light) colormap
	bw		Black & white colormap
-n	Do not show graticule or trigger marks.
--	End of options.
<image.bmp>
	Output image file name. If non given an image viewer is used.
	'-' indicates standard output.
Examples
	# Display snapshot
	gds-snapshot
	# Get snapshot and store it as BMP image
	gds-snapshot snapshot.bmp
	# Get snapshot and convert it to a PNG image
	gds-snapshot - | bmptopnm | pnmtopng > snapshot.png
!
}

function gds-snapshot () {
	local format=1	# use format with graticule and trigger marking
	local colormap=""
	local o
	
	OPTIND=1
	while getopts "hc:n-" o
	do
		case "$o" in
		h)	 _gds-snapshot-usage 2>&1
			return 0
			;;
		c)	colormap="-c $OPTARG"
			;;
		n)	format=0
			;;
		-)	break
			;;
		*)	 _gds-snapshot-usage
			return 1
			;;
		esac
	done
	shift $(($OPTIND - 1))
	[ $# -gt 1 ] && { _gds-snapshot-usage; return 1; }
	# set display output format
	:display:output $format
	if [ $? -ne 0 ] ; then
		_gdsh_imsg gds-snapshot "failed"
		return 1
	fi
	local file
	if [ $# -eq 1 ] ; then	
		file="$1"
		[ "$file" == "-" ] && file=/dev/stdout
	else
		file=`mktemp -t gdsh.snapshot.temp.bmp.XXXXXXXX` || return 1
	fi

	_gdsh_imsg gds-snapshot "writing BMP image to file $file"
	:display:output_ $colormap > "$file" || { rm -f "$file"; return 1; }

	if [ $# -eq 0 ] ; then	
		#
		# Run image viewer in background on temp file
		#
		_gdsh_imsg gds-snapshot "running image viewer ${GDSH_IMGVIEWER:-eog} $file"
		(	${GDSH_IMGVIEWER:-eog} "$file"
			sleep 10
			rm -f "$file"
			#_gdsh_imsg gds-snapshot "temporary file $file removed"
		)& disown
	fi
}



# TODO: Query offsets, and adjust data
#
function _gds-plot-usage () {
cat  >&2 <<!
usage: gds-plot [-h] [-f <format>] [-t <title>] [-c <comment>] [<ch> ...] [--] [<file>]
-h	This help information
-f <format>
	Output (file) format. Every format supported by installed gnuplot. If
	non given the function tries to guess the format from the extension of
	<file>. If <file> is not given the on-screen X11 window output is
	assumed and -f is ignored.
-t <title>
	Title line. If not given oscilloscope type, current date, time.
-c <comment>
	Comment line. If not given empty.
<ch>	Channels 1 ... 4
--	End of options.
<file>	Output file name. If non given output to screen.
!
}

function gds-plot () {
	local type=`gds-type` || return 1	 # Oscilloscope type
	local terminal="x11 title \"$type gds-plot\" enhanced persist"
	local tflag=0
	local file=""
	local title="$type `date`" 
	local o
	local comment=""
	OPTIND=1
	while getopts "c:f:t:" o
	do
		case "$o" in
		c)	comment="\n$OPTARG"
			;;
		f)	terminal="$OPTARG"
			tflag=1
			;;
		t)	title="$OPTARG"
			;;
		h)	 _gds-plot-usage 2>&1
			return 0
			;;
		*)	 _gds-plot-usage
			return 1
			;;
		esac
	done
	shift $(($OPTIND - 1))

	local channels=""
	while [ $# -gt 0 ] ; do
		case "$1" in
		1|2|3|4) channels="$channels $1"
			;;
		--)	shift
			break
			;;
		*)	break
			;;
		esac
		shift
	done
	[ $# -gt 1 ] && { _gds-plot-usage; return 1; }

	if [ $# -eq 1 ] ; then
		#
		# Get file name. If format is not explicitly set
		# try to guess format from file name for some common
		# file name extensions.
		#
		file="$1"
		if [ "$tflag" -eq 0 ] ; then
			case "$file" in
			*.dxf)	terminal=dxf ;;
			*.fig)	terminal=fig ;;
			*.gif)	terminal="gif enhanced" ;;
			*.jpg|*.jpeg)
				terminal="jpeg enhanced" ;;
			*.png)	terminal="png enhanced" ;;
			*.svg)	terminal="svg enhanced" ;;
			*.ps|*.eps)
				terminal="postscript enhanced" ;;
			# TODO: Add more?
			*)	 _gds-plot-usage
				echo "gds-plot: Use -f to specify format for file $file" >&2
				return 2
				;;
			esac
		fi
	fi

	#
	# query device to get more details
	#
	local activechannels=`gds-channels -a` || return 1
	if [  -z "$channels" ] ; then
		# use list of active channels if not specified as arguments
		channels="$activechannels"
	else
		# Check if requested channels are active.
		local i
		local j
		for i in $channels ; do
			local k=0
			for j in $activechannels ; do
				if [ "$i" = "$j" ] ; then
					k=1
					break
				else
					continue
				fi
			done
			if [ $k -eq 0 ] ; then
				echo "gds-plot: Error: Channel $i not active" >&2
				return 1
			fi
		done
	fi
	[ -z "$channels" ] && {
		echo "gds-plot: Error: No channel active and no channel defined on command line" >&2
		_gds-plot-usage
		return 1
	}
	local grid=`:disp:grat_` || return 1 # 0: grid; 1: center cross; 2: off
	local style=`:disp:wav_` || return 1 # 0: vector; 1: dots

	local timswe=`:tim:swe_` || return 1
	if [ $timswe = 4 ] ; then
		#
		# X-Y plot
		# Instrument always returns 5000 data points per
		# channel in X/Y mode. The first row an arbitrarily scaled
		# count, the second the channel's data.
		# Need to merge the two second columns from
		# CH1 and CH2 to get the data.
		#
		{
			echo "set terminal $terminal"
			[ -n "$file" ] && echo "set output \"$file\""
			[ "$grid" -eq 0 ] && echo "set grid"
			if [  "$style" -eq 0 ] ; then
				echo "set style data lines"
			else
				#echo "set style data points"
				#echo "set pointsize 0.7"
				echo "set style data dots"
			fi
			cat <<-!
				set xlabel "Voltage/V" enhanced
				set ylabel "Voltage/V" enhanced
				set style line 1 lt rgb "#ffb000"
				set title "$title$comment" 
				plot '-' t "X-Y" ls 1
			!
			{
				:acq1:mem_ -0 -h -V `:chan1:scal_` || return 1
				echo "---"
				:acq2:mem_ -0 -h -V `:chan2:scal_` || return 1
			} | gawk '
				BEGIN { IFS=","; n=0; i=0 }
				/^---/ { n=0; i=1; next }
				{ data[n++,i] = $2 }
				END { for(n=0; n < 5000; n++) {
					print data[n, 0] " " data[n, 1]
				      }
				}
			'
			echo "e"
		} | ${GDSH_GNUPLOT:-gnuplot} -bg white # ivory
	else 
		#
		# Prepare normal plotting command
		#
		local args=""		# assemble pseudo file arguments
		local sep=""
		local i
		for i in $channels ; do
			args="$args$sep'-' t \"Ch$i\" ls $i" # TODO: append h,v resolution
			sep=", "
		done
		{
			echo "set terminal $terminal"
			[ -n "$file" ] && echo "set output \"$file\""
			[ "$grid" -eq 0 ] && echo "set grid"
			if [  "$style" -eq 0 ] ; then
				echo "set style data lines"
			else
				#echo "set style data points"
				#echo "set pointsize 0.7"
				echo "set style data dots"
			fi
			cat <<-!
				set xlabel "Time/s" enhanced
				set ylabel "Voltage/V" enhanced
				set style line 1 lt rgb "#ffb000"
				set style line 2 lt rgb "#00ffff"
				set style line 3 lt rgb "#ff00c5"
				set style line 4 lt rgb "#66ff66"
				set title "$title$comment" 
				plot $args
			!
			if [ $timswe == 2 ] ; then
				hscale=`:tim:wind:scal_` || return 1
			else
				hscale=`:tim:scal_` || return 1
			fi
			for i in $channels ; do

				# get data for channel, scale horizontally to
				# interval, scale vertically to vertical
				# resolution
				#:acq${i}:mem_ -0 -V `:chan${i}:scal_` |
				#	awk -v "hscale=$hscale" '
				#	BEGIN { sp = hscale / 25 }
				#	{ print $1 * sp " " $2 }
				#	END { print "e" }' || return 1
				:acq${i}:mem_ -0 -h -V `:chan${i}:scal_` || return 1
			done
		} | ${GDSH_GNUPLOT:-gnuplot} -bg white # ivory
	fi
}


#
function _gds-esr-usage () {
cat  >&2 <<!
usage: gds-esr [-h]
-h	This help information
Bits:
PON	Power-on.
URQ	User request.
CME	Command error. Error while parsing a command or query.
EXE	Execution error. Error while executing a command or query.
DDE	Device dependent error.
QYE	Query error. Command or query protocol error.
RQC	Request control.
OPC	Operation complete. Set by *OCP (_ocp) command when all pending
	operations have finished.
!
}

function _gds-esr-decode () {
	gawk '{
		print "PON URQ CME EXE DDE QYE RQC OPC"
		v=$0
		for(i=0; i < 8; i++) {
			b = and(v, 0x80)
			v *= 2
			printf " " !!b "  "
		}
		print ""
	} '
}

function gds-esr () {
	if [ "$1" = "-h" ] ; then
		_gds-esr-usage 2>&1
		return 0
	fi
	echo "Standard event status enable register:"
	_ese_ | _gds-esr-decode
	echo ""
	echo "Standard event status register:"
	_esr_ | _gds-esr-decode
}


#
function _gds-sbr-usage () {
cat  >&2 <<!
usage: gds-sbr [-h]
-h	This help information
Bits:
OPER	Operational event status register summary. Disjunction (OR) of all
	operational status register bits.
MSS (RQS)
	Master summary status, request service. Disjunction (OR) of all
	Status Byte Register bits (this register).
ESB	Standard event status register summary. Disjunction (OR) of all
	enabled standard event status registers (SESRs).
MAV	Message available.
QUES	Questionable event status register summary. Disjunction (OR) of all
	questionable status register bits.
NU	Not used.
!
}
function _gds-sbr-decode () {
gawk '
	{
		print "OPER MSS  ESB  MAV  QUES  NU   NU   NU"
		v=$0
		for(i=0; i < 8; i++) {
			b = and(v, 0x80)
			v *= 2
			printf " " !!b "   "
		}
		print ""
	} '
}

function gds-sbr () {
	if [ "$1" = "-h" ] ; then
		_gds-sbr-usage 2>&1
		return 0
	fi
	echo "Service request enable register:"
	_sre_ | _gds-sbr-decode
	echo ""
	echo "Status Byte Register:"
	_stb_ | _gds-sbr-decode
}

function gds-help() {
	env PAGER="less -s '-p$*'" man 7 gdsh
}

###############################################################################
# Functions related to the operation of gdsh as such, not to the operation
# of the oscilloscope.
# See "man gdsh" for documentation
###############################################################################



#
function _gdsh-timeout-usage () {
cat  >&2 <<!
usage: gds-timeout [-h] [<sec>]
-h	This help information
<sec>	Read timeouts in seconds. If not present the current timeout
	value will be displayed.
!
}

function gdsh-timeout () {
	case "$1" in
	-h)
		_gdsh-timeout-usage 2>&1
		return 0
		;;
	[0-9]*)
		GDSH_READ_TIMEOUT="$1"
		return 0
		;;
	.*)	
		_gdsh-timeout-usage
		return 1
		;;
	esac
	echo ${GDSH_READ_TIMEOUT:-3}
}

function gdsh-version () {
	echo "@version@"
}

function gdsh-warranty () {
	less -G "-p15. Disclaimer of Warranty." "$GDSH_DOCDIR/COPYING"
}

function gdsh-conditions () {
	less -G "-p4. Conveying Verbatim Copies." "$GDSH_DOCDIR/COPYING"
}

$GDSH_DMSG "lib/gdsh.functions loaded"
