#!/bin/sh
# USB reset port functionality. Only one instance of check or reset can run at a time,
# as this script may be triggered multiple times at once by udev.
# HF:
# 2 is top port.
# 1 is bottom port.
#
# Reset a USB port.
# Usage: usb-monitor reset port
#
# Check kernel messages for a USB hub port failure, and calls reset if a failure is found.
# Usage: usb-monitor check port
set -u

source /lib/functions/navico_env.sh
source /lib/functions/gpio.sh
source /etc/rc.d/rc.common

navico_env_init

kernel_time_fromline() {
    echo "$1" | sed -n 's/\[[ ]*\([0-9]*\)\..*\].*/\1/p'
}


kernel_time_current() {
    cat /proc/uptime | sed 's/\..*//'
}


usb_cycle_port() {
    local port="$1"
    local cmd
    local device
    local usb_driver_path="/sys/bus/platform/drivers/imx_usb"
    local usb_hub_reset

    case "${FAMILYID}-$port" in
    Hatchetfish-1) # Bottom
        mcu_cmd="t 36 1 0"
        ;;
    Hatchetfish-2) # Top
        mcu_cmd="t 36 0 1"
        ;;
    Bluefin-2)
        usb_hub_reset="$(gpio_idx usb_hub_reset)"
        ;;
    Lisa-*)
        # Lisa has ony one CPU and both ports are connected to it over one USB hub.
        # Because of the way the hub is powered, the is no reliable way to reset
        # one port at a time, so both ports need to be reset.
        mcu_cmd="t 36 1 1"
        # Both ports are connected to the same device in Linux. Port=1 needs to be
        # overriden, so that the other usb hub is not unbound in following steps.
        port=2
        ;;
    esac

    if [ "${PROCESSOR_FAMILY}" = "mx6" ]; then
        device="$(find /sys/devices -name usb${port} | sed -n 's/.*\/\(.*\.usb\)\/.*$/\1/p')"
    fi

    if [ -z "${device:-}" ]; then
        printf "error: cannot find usb port $port to reset\n" >&2
        return 1
    fi

    echo "Resetting USB port $port"
    echo "$device" > "${usb_driver_path}/unbind"
    echo "$device" > "${usb_driver_path}/bind"

    if [ -n "${mcu_cmd:-}" ]; then
        echo "$mcu_cmd" | mcu-util
    fi

    if [ -n "${usb_hub_reset:-}" ]; then
        gpio_set_value "$usb_hub_reset" 1
        sleep 0.5
        gpio_set_value "$usb_hub_reset" 0
    fi

    sleep 2
}


usb_reset_monitor() {
    local port=$1
    local dmesg_log=/tmp/reset-${port}-dmesg.log
    local current_time=$(kernel_time_current)
    local period=3 # seconds in the past

    sleep $period

    dmesg > "$dmesg_log"
    local last_error="$(grep "hub ${port}-1.*err.*" "$dmesg_log" | tail -n1)"

    if [ -n "$last_error" ]; then
        local line_time="$(kernel_time_fromline "$last_error")"
        local min_time=${LAST_ERROR_TIME:-$((current_time - period))} # Minimum time within which an error can occur

        if [ $line_time -ge $min_time ]; then
            printf "error on usb re-init, reset port ${port}\n"
            usb_cycle_port $port
        fi
    fi
    rm -rf "$dmesg_log"
}


hf_reset_main() {
    local port=$1
    stdio_syslog_start "$2"

    # Get the current USB mux
    local mux_cfg="$(echo "t 10" | mcu-util | cut -d' ' -f1-2 | sed -e 's/ /\t/g')"

    # Determine CPU mux of the disconnected port
    local port_mapping=$(echo "$mux_cfg" | cut -f "$port")

    # mux=0 -- CPU B
    # mux=1 -- CPU A
    local cpu_mux_state
    case "$SUBFAMILYID" in
    1)
        cpu_mux_state="1"
        ;;
    2)
        cpu_mux_state="0"
        ;;
    esac


    if [ "$port_mapping" = "$cpu_mux_state" ]; then
        usb_reset_monitor $port &
        local monitor_pid=$!
        usb_cycle_port $port
        wait $monitor_pid
    fi

}


reset_main() {
    local port=$1
    stdio_syslog_start "$2"

    usb_reset_monitor $port &
    local monitor_pid=$!
    usb_cycle_port $port
    wait $monitor_pid
}


check_main() {
    local port=$1
    local period=10 # seconds in the past
    local current_time=$(kernel_time_current)
    local dmesg_log=/tmp/check-${port}-dmesg.log

    stdio_syslog_start "$2"

    sleep 5 # udev event may occur before kernel message

    dmesg > "$dmesg_log"
    local last_error="$(grep "hub ${port}-1.*hub_port_status failed.*" "$dmesg_log" | tail -n1)"

    if [ -n "$last_error" ]; then
        local line_time="$(kernel_time_fromline "$last_error")"

        if [ $line_time -ge $((current_time - period)) ]; then
            printf "usb hub error on port ${port}, resetting\n"
            LAST_ERROR_TIME=$((line_time+1)) $0 reset $port
        fi
    fi

    rm "$dmesg_log"
}


print_help() {
    printf "Usage: %s [ reset port | check port ]\n" "${0##*/}" >&2
}


case ${1:-} in
    reset|check)
        if [ -z "${2:-}" ]; then
            print_help
            exit 1
        fi

        if [ "${1:-}" = reset -a "$FAMILYID" = Hatchetfish ]; then
            handler_base=hf_reset
        else
            handler_base=$1
        fi

        port="$2"
        process_name="$(basename $0)-${1}-${port}"
        PIDFILE="/run/${process_name}.pid"
        SERVICE_HANDLER=${handler_base}_main start_service $port "$process_name"
        ;;

    *)
        print_help
        exit 1
        ;;
esac
