#!/bin/bash

# This script performs several actions on a modem, usually called
# by udev when the modem is plugged.
#
# The status of the modem is stored in "$statusfile" (see below).
# Upon initialization the PIN is entered if necessary and available.
# Status changes are logged to syslog (facility daemon)
#
# Usage: modem-init.sh [options] <device node>
#
# Options:
#
#  -u		Do not use locking
#  -s		Set the tty parameters (stty)
#  -i		Initializes the modem
#  -p <newpin>	Resets the PIN to the new value, PUK required,
#		returns 2 if PUK incorrect, 1 on other error
#  -P <puk>	Provides the PUK
#  -c <cmd>	Send <cmd> to the modem and print modem output to stdout
#		The <cmd> should not be prefixed with "AT"
#  -d <name>	The exclusive name of the device under /dev, it is an error
#		if this option is given and <name> already exists
#
#

# Make sure we have a sane PATH
export PATH=/usr/bin:/bin:/usr/sbin:/sbin

statusdir="/dev/.modem-data"
statusfile="$statusdir/status"
pinfile="/etc/chatscripts/modem-pin"
ifwpidfile="/var/run/ifwatchdog.pid"

# global state variables
stty_done=
start_done=
need_restore=

function set_status()
{
    echo "$1" > "$statusfile"
    logger -p daemon.info -t modem-init -- "modem status = $1"
}

function save_response()
{
    local response=''
    while read line
    do
      if [ -n "$line" -a "$line" != 'OK' ]; then
	  if [ -z "$response" ]; then
	      response="$line"
	  else
	      response="$response $line"
	  fi
      fi
    done < <(echo "$1")
    # remove all CR on output
    echo "${response//$'\r'/}" > "$statusdir/$2"
}

function stty_sane()
{
    [ -z "$stty_done" ] || return 0
    stty -F "$devnode" sane -icrnl || return 1
    stty_done=1
    return 0
}

function start()
{
    [ -z "$start_done" ] || return 0

    logger -p daemon.info -t modem-init -- "start '$devnode'"

    stty_sane || return 1

    # Initialize the modem, should disable echoing (E0) for later commands
    # Use factory defaults to avoid any weird user stored default config and
    # retry reset to factory defaults on failure to get OK
    chat -v -t 5 ABORT ERROR '' 'AT&F0' 'OK\r-AT&F0-OK\r' ATQ0V1E0 'OK\r' < "$devnode" > "$devnode" || \
	return 1

    need_restore=1

    # Set the error messages from mobile termination to be +CME ERROR: <code>, if possible
    chat -v -t 2 ABORT ERROR '' 'AT+CMEE=1' 'OK\r' < "$devnode" > "$devnode"

    start_done=1

    return 0
}

function restore()
{
    # make sure we leave default command processing parameters
    chat -v -t 2 ABORT ERROR '' ATQ0V1E1 'OK\r' < "$devnode" > "$devnode"
}

function data()
{
    # Get the modem ID strings
    out="$(chat -v -t 2 -e ABORT 'ERROR\r' '' 'AT+GMI' 'OK\r' < "$devnode" 2>&1 > "$devnode")" && \
	save_response "$out" "manufacturer"

    out="$(chat -v -t 2 -e ABORT 'ERROR\r' '' 'AT+GMM' 'OK\r' < "$devnode" 2>&1 > "$devnode")" && \
	save_response "$out" "model"

    out="$(chat -v -t 2 -e ABORT 'ERROR\r' '' 'AT+GMR' 'OK\r' < "$devnode" 2>&1 > "$devnode")" && \
	save_response "$out" "revision"

    out="$(chat -v -t 2 -e ABORT 'ERROR\r' '' 'AT+GSN' 'OK\r' < "$devnode" 2>&1 > "$devnode")" && \
	save_response "$out" "serial"

    return 0
}

function data_sim()
{
    out="$(chat -v -t 2 -e ABORT 'ERROR\r' '' 'AT+CIMI' 'OK\r' < "$devnode" 2>&1 > "$devnode")" && \
	save_response "$out" "imsi"

    return 0
}

function clean()
{
    [ -e "$statusdir" ] && rm -rf "$statusdir"
    mkdir "$statusdir"
}

function check_pin()
{
    local RET

    # the order of ABORT strings is related to the result codes below
    chat -v -t 5 ABORT 'PIN\r' ABORT 'PIN2\r' ABORT 'PUK\r' ABORT 'PUK2\r' \
	ABORT 'ERROR\r' '' 'AT+CPIN?' 'READY\r' < "$devnode" > "$devnode"
    RET=$?

    if [ "$RET" -eq 0 ]; then
	# got "READY", no PIN required
	set_status "READY"
    elif [ "$RET" -eq 4 ]; then
	# got "PIN", the modem is expecting a PIN
	set_status "PIN"
    elif [ "$RET" -eq 5 ]; then
	# got "PIN2", the modem is expecting a secondary PIN
	set_status "PIN2"
    elif [ "$RET" -eq 6 ]; then
	# got "PUK", the modem is expecting the PUK
	set_status "PUK"
    elif [ "$RET" -eq 7 ]; then
	# got "PUK2", the modem is expecting the secondary PUK
	set_status "PUK2"
    else
	# some error occured
	set_status "ERROR"
	return 1
    fi

    return 0
}

function enter_pin()
{
    local RET

    [ -s "$pinfile" ] || return 1
    pin="$(< "$pinfile")" || return 1

    # Escape \ and " as required in AT strings
    pin="${pin//\\/\\5C}"
    pin="${pin//\"/\\22}"

    chat -v -t 2 ABORT 'ERROR\r' ABORT '+CME ERROR: 16\r' \
	ABORT '+CME ERROR: incorrect password\r' \
	'' "AT+CPIN=\"$pin\"" 'OK\r' < "$devnode" > "$devnode"
    RET=$?
    if [ "$RET" -eq 5 -o "$RET" -eq 6 ]; then
	set_status "PIN INCORRECT"
	return 2
    fi
    [ "$RET" -ne 0 ] && return 1
    return 0
}

function init()
{
    logger -p daemon.info -t modem-init -- "initializing '$devnode'"

    clean || return 1
    start
    if [ $? -ne 0 ]; then
	set_status "ERROR"
	return 1
    fi
    data
    check_pin || return 1
    if [ "$(< "$statusfile")" == "PIN" ]; then
	enter_pin
	if [ "$?" -ne 2 ]; then
	    check_pin || return 1
	fi
    fi
    if [ "$(< "$statusfile")" == "READY" ]; then
	data_sim
	# Signal ifwatchdog that modem is ready, it may start trying again
	if [ -s "$ifwpidfile" ]; then
	    kill -HUP "$(< "$ifwpidfile")" 2>/dev/null
	fi
    fi
    return 0
}

function set_new_pin()
{
    local RET
    local puk="$1"
    local newpin="$2"
    local status

    # Is a new PIN expected ?
    [ -f "$statusfile" ] || return 1
    status="$(< "$statusfile")"
    [ "$status" == "PUK" -o "$status" == "PUK INCORRECT" ] || return 1

    # Escape \ and " as required in AT strings
    puk="${puk//\\/\\5C}"
    puk="${puk//\"/\\22}"
    newpin="${newpin//\\/\\5C}"
    newpin="${newpin//\"/\\22}"

    logger -p daemon.info -t modem-init -- "resetting pin through '$devnode'"

    # Attempt to set new pin
    chat -v -t 2 ABORT 'ERROR\r' ABORT '+CME ERROR: 16\r' \
	ABORT '+CME ERROR: incorrect password\r' \
	'' "AT+CPIN=\"$puk\",\"$newpin\"" 'OK\r' < "$devnode" > "$devnode"
    RET=$?
    if [ "$RET" -eq 5 -o "$RET" -eq 6 ]; then
	set_status "PUK INCORRECT"
	return 2
    elif [ "$RET" -ne 0 ]; then
	RET=1
    fi

    # Update the status
    check_pin

    # Get the SIM data if now available
    if [ "$(< "$statusfile")" == "READY" ]; then
	data_sim
    fi

    return $RET
}

function send_cmd()
{
    local cmd="$1"
    # chat sends output to stderr, which we redirect to our stdout
    chat -e -v -t 1 ABORT ERROR '' "AT${cmd}" 'OK\r' < "$devnode" 2>&1 > "$devnode"
}

function process()
{
    if [ -n "$devname" -a -e /dev/"$devname" ]; then
	echo "Device '$devname' already exists, aborting" >&2
	return 1
    fi
    if [ -n "$do_stty" ]; then
	stty_sane || return
    fi

    if [ -n "$do_init" ]; then
	init || return
    fi

    if [ -n "$do_pinreset" ]; then
	start || return 1
	set_new_pin "$puk" "$newpin" || return
    fi

    if [ -n "$do_cmd" ]; then
	restore || return 1
	send_cmd "$cmd" || return
    fi

    return 0
}

#
# Main
#

no_lock=
do_stty=
do_init=
do_pinreset=
do_cmd=
devname=
newpin=
puk=

while getopts "usip:P:c:d:" opt; do
    case "$opt" in
	u)
	    no_lock=1
	    ;;
	s)
	    do_stty=1
	    ;;
	i)
	    do_init=1
	    ;;
	p)
	    do_pinreset=1
	    newpin="$OPTARG"
	    ;;
	P)
	    puk="$OPTARG"
	    ;;
	c)
	    do_cmd=1
	    cmd="$OPTARG"
	    ;;
	d)
	    devname="$OPTARG"
	    ;;
	\?)
	    exit 1
	    ;;
    esac
done
shift $((OPTIND-1))

devnode="$1"

if [ -z "$devnode" ]; then
    echo "Missing device node '$devnode'" >&2
    exit 1
fi

if [ -n "$do_pinreset" -a -z "$puk" ]; then
    echo "PUK required to reset PIN" >&2
    exit 1
fi

[ -n "$no_lock" ] || lockdev -l "$devnode"
if [ $? -ne 0 ]; then
    echo "Failed locking '$devnode'" >&2
    exit 1
fi

process
RET=$?

[ -z "$need_restore" ] || restore

[ -n "$no_lock" ] || lockdev -u "$devnode"
if [ $? -ne 0 ]; then
    echo "Failed unlocking '$devnode'" >&2
fi

exit "$RET"
