#!/usr/bin/env bash

# default port
PORT=/dev/ttyACM0

# buggy devices to skip
buggy=("/dev/sg0" "/dev/hidraw0" "/dev/usb/hiddev0")


check_port() {
	[[ ! -z $1 ]] && PORT=$1
	if [[ " ${buggy[@]} " =~ " $PORT " ]]; then
		echo "Skipping buggy detected device: $PORT"
		exit 3
	fi
	[[ ! -c $PORT ]] && echo "$PORT is not a character device" && exit 2
	echo "Using $PORT"
}


watchdog_query() {
	stty -F $PORT 9600 raw -echo || return 1
	DMPLOG=$(mktemp /tmp/wdlog.XXXXXX) || return 2
	for (( i=1; i <= 10; i++ )); do
		exec {fd}< $PORT
		cat <&${fd} > $DMPLOG &
		echo -n "$1" > $PORT
		exitcode=$?
		sleep 0.1s
		kill -9 $!
		wait $!
		exec {fd}<&-
		[[ $exitcode -ne 0 ]] && break
		reply=`cat $DMPLOG | cut -d"~" -f 2`
		[[ ! -z "$reply" && "$reply" != "A" ]] && break
		sleep 0.2s
	done
	rm -f $DMPLOG
	[[ ! -z "$reply" ]] && echo "$reply" && return 0
	[[ $exitcode -ne 0 ]] && return 3
	return 4
}


watchdog_nowarn() {
	# output watchdog replies to /dev/null to avoid ch341 errors
	stty -F $PORT 9600 raw -echo && 
		exec {fd}< $PORT && 
		cat <&${fd} > /dev/null &
	# kill background processes if any on exit
	trap "trap - SIGTERM && kill -- -$$" SIGINT SIGTERM EXIT
}


watchdog_ping() {
	#if port 22 (ssh) responds then
	#send ping signal to WD, if WD does not receive it for a while, then WD RESET the MB.
	local errors=0
	local pinging=1
	local port=22
	t2=`date +%s` || t2=0
	watchdog_nowarn
	while true
	do
		t1=$t2
		if [[ $pinging -eq 1 ]] && nc -w 1 -z localhost $port 2>&1; then
			echo "Pinging watchdog"
			echo -n "~U" > $PORT && errors=0 || ((errors++))
			(( errors > 10 )) && break
		else
			logger -s -t "$(basename "$0")" "NOT pinging watchdog - sshd port: $port, enabled: $pinging"
			port=`grep -oP "^Port[\s]+\K[0-9]+" /etc/ssh/sshd_config 2>/dev/null | tail -n 1`
			[[ -z $port ]] && port=22
		fi
		sleep 5
		t2=`date +%s` || t2=0
		[[ $t2 -eq 0 || $t1 -eq 0 || $(( t2 - t1 )) -gt 10 ]] && pinging=0 || pinging=1
	done

	echo "$PORT" | message error "opendev wd error" payload
	exit 1
}


watchdog_settings_decode() {
	#
	# Configuration string deserialization
	#
	# Watchdog Lite
	#
	# #		unit		desc
	# --------------------------------------------------------------------------------------------
	# 1		1 мин*		Ожидания сигнала перезагрузки (t1).
	# 2		100 мс*		Длительность импульса сигнала «Reset» (t2). Для версии выпуском позднее 07.2017.
	#					*значения параметров могут быть в диапазоне 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A(10), B(11), C(12), D(13), E(14), F(15).
	#
	# Watchdog Pro2
	#
	# #		unit		desc
	# --------------------------------------------------------------------------------------------
	# 1		1 мин*		Ожидания сигнала перезагрузки (t1).
	# 2		100 мс*		Длительность импульса сигнала «Reset» (t2).
	# 3		1 с*		Длительность импульса сигнала «Power» (t3).
	# 4		1 с*		Длительность ожидания (t4).
	# 5		100 мс*		Длительность импульса сигнала «Power» (t5).
	# 6					Режим канала 1: 0 - выкл, 1 - RESET, 2 - POWER, 3 - управляемый (нач. сост. - открыт), 4 - управляемый (нач. сост. - закрыт).
	# 7					Режим канала 2: 0 - выкл, 1 - RESET, 2 - POWER, 3 - управляемый (нач. сост. - открыт), 4 - управляемый (нач. сост. - закрыт).
	# 8					Ограничение количества перезагрузок. 0 - нет ограничений.
	# 9					Режим канала 3 (Вх/In): 0 - выкл, 1 - дискретный вход, 3 - вход датчика температуры ds18b20.
	# 10				Пороговое значение температуры для автоматического перезапуска. Актуально при канале 3 (Вх/In), установленном в режим опроса датчика температуры. Задаётся значением пороговой температуры в шестнадцатеричном формате, например: 32 градуса - 20, 80 градусов - 50, 00 - отключено.
	#					*значения параметров 1-5 могут быть в диапазоне 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A(10), B(11), C(12), D(13), E(14), F(15).


	# args

	local -r settings_string_raw="${1-}"


	# consts

	local -r -i settings_string_size=${#settings_string_raw}
	local -r long_line='--------------------------------------------------------------------------------------------------------'


	# defaults

	local -r watchdog_version_DEFAULT='unknown'
	local -r -i chunk_size_DEFAULT=1
	local -r title_DEFAULT='default title'
	local -r -i unit_multiplier_DEFAULT=1


	# dicts

	local -r -a watchdog_version_dictionary=(
		[2]='Lite, before 07.2017'
		[3]='Lite'
		[12]='Pro2'
	)

	local -r -A settings_dictionary=(
		# A simple associative dictionary with [offset.entity] structure. Entities are:
		# title
		# size (default: 1 char)
		# unit (default: empty)
		# unit_multiplier (default: 1)
		# valueN (default: none)

		['0.title']='Always 15 (0x0F)'

		['1.title']="Time interval before RESET signal (t1)"
		['1.unit']='min'

		['2.title']='RESET signal duration (t2)'
		['2.unit']='ms'
		['2.unit_multiplier']='100'

		['3.title']='POWER-OFF signal duration (t3)'
		['3.unit']='sec'

		['4.title']='Pause between POWER signals (t4)'
		['4.unit']='sec'

		['5.title']='POWER-ON signal duration (t5)'
		['5.unit']='ms'
		['5.unit_multiplier']='100'

		['6.title']='Channel 1 (Output)'
		['6.value0']='<not used>'
		['6.value1']='RESET'
		['6.value2']='POWER'
		['6.value3']='managed (initial state: OPENED)'
		['6.value4']='managed (initial state: CLOSED)'

		['7.title']='Channel 2 (Output)'
		['7.value0']='<not used>'
		['7.value1']='RESET'
		['7.value2']='POWER'
		['7.value3']='managed (initial state: OPENED)'
		['7.value4']='managed (initial state: CLOSED)'

		['8.title']='Limit RESET attempts'
		['8.unit']='times'
		['8.value0']='no limit'

		['9.title']='Channel 3 (Input)'
		['9.value0']='<not used>'
		['9.value1']='Digital input'
		['9.value3']='Temperature sensor (ds18b20)'

		['10.title']='Temperature threshold for RESET'
		['10.size']=2
		['10.unit']='C'
		['10.value0']='<not used>'
	)


	# code

	printf 'Watchdog version: %s (%d chars)\n' "${watchdog_version_dictionary[settings_string_size]:-${watchdog_version_DEFAULT}}" "$settings_string_size"
	printf "Raw configuration: '%q', decoding:\n" "$settings_string_raw"
	printf '%s\n' "$long_line"
	printf '| %2.2s | %40.40s | %-40.40s | %3.3s | %3.3s |\n' '#' 'Description' 'Decoded value' 'DEC' 'HEX'
	printf '%s\n' "$long_line"

	# let's deserialize
	for (( offset=0; offset < settings_string_size; )); do

		# get the chunk
		chunk_size="${settings_dictionary[${offset}.size]:-${chunk_size_DEFAULT}}"
		chunk_raw="${settings_string_raw:${offset}:${chunk_size}}"

		title="${settings_dictionary[${offset}.title]:-${title_DEFAULT}}"

		# let's validate for hex numbers (regex: only hexes, at least one)
		if [[ $chunk_raw =~ ^[[:xdigit:]]{1,}$ ]]; then
			char_in_hex="${chunk_raw^^}"
			char_in_dec="$(( 16#${char_in_hex} ))"

			# first we'll go for enums, then for units
			# are there enum values in the dictionary?
			if [ -n "${settings_dictionary[${offset}.value${char_in_dec}]}" ]; then
				# yes, we've got enums, let's use them
				decoded_value="${settings_dictionary[${offset}.value${char_in_dec}]}"
			else
				# no enums. are there units in the dictionary?
				if [ -n "${settings_dictionary[${offset}.unit]}" ]; then
					# yes, we've got units, let's use them
					unit_name="${settings_dictionary[${offset}.unit]}"
					unit_multiplier="${settings_dictionary[${offset}.unit_multiplier]:-${unit_multiplier_DEFAULT}}"
					value=$(( char_in_dec * unit_multiplier ))
					decoded_value="$value $unit_name"
				else
					# enums: none, units: none. nothing to decode.
					decoded_value='-'
				fi
			fi
			printf '| %2.2s | %40.40s | %-40.40s | %3d |  %2.2s |\n' "$offset" "$title" "$decoded_value" "$char_in_dec" "$char_in_hex"
		else
			# didn't validated, print some diagnostic info
			chunk_raw_ascii="$( printf '%d' "'$chunk_raw" )" # get the ascii-code of the symbol
			#                                ^ and yes it's fully legit, trust me
			printf '| %2.2s | %40.40s | %-40.40s | %3.3s |  %2.2s |\n' "$offset" "$title" "ERR: '$chunk_raw' (ASCII $chunk_raw_ascii) is not a hex number" '-' '-'
		fi
		(( offset += chunk_size ))
	done

	printf '%s\n' "$long_line"
}


watchdog_decode_temperature() {
	raw_answer_string="$1"
	# check for valid temperature. not sure there're negative numbers, but why not?
	# regex is: 1st=digit or '-'; 2nd=digit; 3rd=digit; 4th=digit
	if [[ "$raw_answer_string" == "G0000"  ]]; then
		echo "0°C or temperature sensor is not attached"
	elif [[ "$raw_answer_string" == 'GEEEE' ]]; then
		echo "Error or temperature sensor is not attached"
	elif [[ $raw_answer_string =~ ^G(-|[[:digit:]])[[:digit:]]{3}$ ]]; then
		# watchdog gives us 5 chars: 'G' + temperature multiplied by 10. let's decompose:
		temperature_with_sign="${raw_answer_string:1}" # strip 'G'
		# decouple number and its sign
		if [[ "${temperature_with_sign:0:1}" == '-' ]]; then
			sign='-'; unsigned_temperature="${temperature_with_sign:1}" # strip sign
		else
			sign='+'; unsigned_temperature="${temperature_with_sign}" # do nothing
		fi
		printf '%.1f °C\n' "$((10**9 * ${sign}10#${unsigned_temperature}/10))e-9"
	else
		echo "Unknown error, answer is '$raw_answer_string'"
	fi
}


case $1 in
	reset)
		check_port $2
		echo "Pushing Reset"
		echo -n "~T1" > $PORT
	;;

	power)
		check_port $2
		echo "Pushing Power"
		echo -n "~T2" > $PORT
	;;

	poweroff)
		check_port $2
		echo "Turning power off completely"
		echo -n "~T3" > $PORT
	;;

	ping)
		check_port $2
		watchdog_ping
	;;

	fw|firmware)
		check_port $2
		echo "Reading firmware version"
		query="$( watchdog_query '~I' )" || exit
		[[ $query =~ ^I[0-9]{3}$ ]] &&
			echo "${query/I/v} - Bitroleum watchdog (Red PCB)" ||
			echo "${query/I/v} - Opendev watchdog (Black PCB)"
	;;

	read)
		check_port $2
		echo "Reading settings"
		query="$(watchdog_query '~F')" || exit
		echo "$query"
	;;

	settings)
		check_port $2
		echo "Reading settings"
		query="$( watchdog_query '~F' )" || exit
		watchdog_settings_decode "$query"
	;;

	temp|temperature)
		check_port $2
		echo "Reading temperature"
		query="$( watchdog_query '~G' )" || exit
		watchdog_decode_temperature "$query"
	;;

	decode)
		watchdog_settings_decode "$2"
	;;

	write)
		check_port $3
		query="$(watchdog_query "~F")" || exit
		[[ ! $2 =~ ^F[0-9A-Fa-f]{$(( ${#query} - 1 ))}$ ]] && echo "Settings format is wrong. It has to be like '$query'" && exit 1
		[[ "$2" == "$query" ]] && echo "These settings are already written: '$query'" && exit 0
		echo "Writing settings"
		echo "Old settings: '$query'"
		echo "New settings: '$2'"
		query=`watchdog_query "~W${2/F}"` || exit
		[[ "$query" == "$2" ]] &&
			echo "Done" ||
			echo "Now settings: '$query' - incorrect parameter or firmware limitation"
	;;

	disable|pause)
		check_port $2
		echo "Disabling watchdog"
		query="$( watchdog_query '~P1' )" || exit
		[[ $query == "P1" ]] &&
			echo "Done" ||
			echo "Unexpected response: $query"
	;;

	enable|resume)
		check_port $2
		echo "Enabling watchdog"
		query="$( watchdog_query '~P0' )" || exit
		[[ $query == "P0" ]] &&
			echo "Done" ||
			echo "Unexpected response: $query"
		
	;;

	*)
		name=`basename $0`
		echo "Usage: $name  ping|reset|power [port]"
		echo "       $name  fw|read|settings [port]"
		echo "       $name  temp|temperature [port]"
		echo "       $name  write <settings> [port]"
		echo "       $name  decode <settings>"
		echo "       $name  enable|disable [port]"
		echo "       $name  poweroff [port]"
		exit 1
	;;
esac