#!/bin/bash

# gemplug
#
time_stamp="Time-stamp: <2011-03-30 18:26:24 pete>"

#
# Control multiple Gembird power outlets via sispmctl
#
# (c) Heike C. Zimmerer <hcz@hczim.de>
#
# This program is licensed as follows:
#
LICENSE="\
    This program 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.

    This program 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 this program.  If not, see <http://www.gnu.org/licenses/>."

# History:
#   2008-04-23   v42.03 hcz first public release
#      listOneOutlet: section was listed twice
#      rc file: tried to write inte /etc when rc file was located there
#      $_h/$_x: hour in interval spec was messed up
#      more permission cleanup when run with config in /etc/gemplug
#      don't allow the same pos|serial to be referenced more than onec
#   2008-04-24   v42.04 hcz
#      "path =" statement added
#      Changes to rcfile updates
#   2008-04-28   v42.05 hcz
#      -m, -L, pre-off, post-off, pre-on, post-on
#   2008-05-07   v42.06 hcz
#      any sispcmtl command is now re-checked and re-executed (if needed).
#       (Don't know if the spurious errors I'm enconutering are caused
#        by flaky Hardware here or if its is a more basic problem with
#        sispmctl or one of its libs.)
#      Included gemplug version number into file last_sispmctl_S so any
#      version change will trigger a rebuild of the configuration data.
#   2008-05-23  v42.07 hcz
#      Locking code revised.  Re-execute sispmctl on error return.
#   2008-06-16  v42.08 hcz
#      Barfed when started for the 1st time (without a config file).
#      Fixed.
#   2008-06-24  v42.09 hcz
#      -e option added.
#   2008-06-27  v42.10 hcz
#     option -g: result code reversed
#   2008-07-02  v42.11 hcz
#      compiled_rc was unneccessarily witten way too often
#   2008-07-06  v42.12
#      get $HOME from /etc/passwd if undefined (e.g., during system
#      startup)
#   2008-07-09  v42.13
#      still an error when $HOME was undefined.  Fixed.
#   2008-07-09  v42.14
#      use full path to gemplug command in at jobs if available
#   2008-08-22  v42.15
#      typo in usage text corrected
#   2008-10-14  v42.16
#      time format returned by 'at' differs between versions.
#      Function fillPending_jobs changed to accept about any format.
#      LC_ALL is now set at any incocation of 'at'.
#      get_usb_pos: traverse: var 'level' made local.
#   2011-03-30  v42.17 pete
#      make gemplug work with sispmctl 3.0 (new -s output-layout)
version="gemplug version 42.17"


# TODO:        parser: allow empty outlets (will not be shown)
#              man: -e
#              install.sh: x permissins on gemplug

usage(){
    echo "\
Usage:
  $pname [-opts] [<outlet-name>]
     lists available outlets along with their state
  $pname [-opts]  <outlet-name> <state>
     Switch Gembird power outlet
  $pname [-opts]  <outlet-name> <state> -e <command ...>
     Switch Gembird power outlet, execute command, switch back
  $pname [-opts]  <outlet-name> <state>  [at|after <spec>] [until|for <spec>]
     Switch Gembird outlets to <state> and reverse at specified times

  Time specifications must be enclosed in quotes if they contain
    white space.
  'at' and 'until' expect time-of-day specifications.  Any format
    which is acceptable by the 'date -d' command may be used.
  'after' and 'for' expect a time interval: days, hours and minutes in
    descending order, with at least one element marked as being d[ay],
    h[our], m[inute] or a ':' (separator between hours and minutes).
    Elements may be omitted as long as their order is preserved.

opts:
  job-related:
   -j [<outlet-name> [...]]  list pending jobs (default: all)
   -k <outlet-name> [...]    kill pending jobs (outlet-name=ALL kills all)
   -p <job-num> [...]    kill pending jobs by job number
   -a   new jobs are to be added to already pending jobs
   -r   remove pending jobs
   -t   test: don't actually start jobs
   -e <command ...> (3rd argument, after outlet name and state):
        switch, execute command, then switch opposite
  output control:
   -l [<outlet-name> [...]]  list multiple outlets
   -d [<outlet-name> [...]]  dump configuration data
   -v[v[v]]                  verbose reporting
   -c   don't use colors in output
   -n   report states numerical (0, 1 instead of off, on)
   -q   quiet operation: just results and errors
   -g <outlet-name>  set exit code to 0 if outlet active, else 1
  misc:
   -m <outlet-name> [<outlet-name>...] 0|1|on|off    set multiple outlets
   -f   force reconfiguration (redisplay non-fatal errors)
   -C   set world R/W permissions on all devices (requires root privileges)
   -T   write templates for unconfigured devices to stdout
   -TW  append templates for unconfigured devices to configuration file
   -x   script debug (set -x)
   -X
   --license     display license information
   -L, --nolock  don't acquire lock
   --version     display version info
   -h, --help    display this help

Examples:
$pname laser 1 for 1:00h                # switch on now, 1 hour later off
$pname laser 1 at 12:00 for 10min       # switch on at 12:00, off 12:10
$pname laser 0 after 12min for '2d 12h' # off +12min, on now+(2d 12h 12min)
$pname laser 0 until 'Mon 7:00'         # off now until next Monday 7:00"
    exit
}

## global settings
dev_bus_usb="/dev/bus/usb"      # start of USB device node tree
at_queue='d'                    # at Queue (higher letters mean less priority)

# user-editable config
my_rc_file="gemplugrc"

# local config files
last_sispmctl_S="last_sispmctl_S"
my_compiled_rc_file="compiled_rc"
lockdir="/var/lock/gemplug"
pidfile="$lockdir/pid"

# more locals
sispmctl="/usr/local/bin/sispmctl"
my_name="gemplug"              # name for this program (to be used in at jobs)
my_path="${0%/*}"              # path - " -

sudo=              # prefix sispmctl commands by that (default: empty)

# if called from init.d, we don't have a $HOME
if [ -z "$HOME" ]; then
    export HOME="$(
        getent passwd $(id -u) \
        | (IFS=: read user pwd id1  id2 name HOME rest;
           echo "$HOME")
    )"
fi
[ -z "$HOME" ] && HOME=/tmp     # last resort
export HOME
rcdirs=("$HOME/.$my_name" "/etc/$my_name")
if [ -d "$HOME/.config" ]; then
    rcdirs=("$HOME/.config/$my_name" "${rcdirs[@]}")
fi

default_HOME="/root"  # If there's no $HOME, try if /root is writable
## end global settings

# some regexp shortcuts:
_s='[ 	]'                       # space TAB
_S='[^ 	]'                       # not space TAB
_c='(^|[ 	])[;#]'                         # comment start chars
_nl=$'\n'
_h='h(o(u(rs?)?)?)?'             # adds 3 round brackets into regex
_m='m(i(n(u(t(es?)?)?)?)?)?'     # +5
_d='d(a(ys?)?)?'                 # +2
_n='[0-9]+'
_x='[0-9a-f]+'
_s='[ 	]'


vCmd(){
    echo >&2 ": $*"
    "$@"
}

getColors(){
    have_colors=1
    bold=$(tput bold)
    color_reset=$(tput op)$(tput sgr0) # Reset color + attribs
    for color in "black 0" "red 1" "green 2" "brown 3" \
        "blue 4" "magenta 5" "cyan 6" "grey 7"; do
        set -- $color
        eval $export color_$1='$(tput setaf $2)'
        eval $export color_$1_bg='$(tput setab $2)'
        eval $export color_bold_$1="\$bold\$color_$1"
        # eval $export color_bold_$1_bg="\$bold\$color_${1}_bg"
    done

} 2>/dev/null

needColors(){
    [ -z "$TERM$have_term" ] || [ "$have_colors$opt_c" ] || getColors
}


abort(){
    rmLock
    needColors
    echo >&2 "$color_bold_red$pname: $*$color_reset"
    [ "${#at_registered_dates[@]}" -gt 0 ] && echo >&2 "No scheduled action activated"
    kill $$                     # abort from subshells too (kill main)
    sleep 1
    exit 126                    # (usually) not reached
}

min(){
    local min="$1" i
    for i; do
        [ "$i" -lt "$min" ] && min="$i"
    done
    echo "$i"
}

rmLock(){
    # Syntax: rmLock
    # removes lock only if it's ours
    if [ "$(cat 2>/dev/null "$pidfile")" = "$$" ]; then
        #needColors
        #echo "${color_bold_grey}${color_magenta_bg}Removing lock$color_reset"
        rm -rf "$lockdir"
    fi
}

needLock(){
    # Syntax: needLock <number of seconds to wait>
    # get a lock before executing sispmctl if we don't own it already
    #  Note: sets global 'have_lock' - so make sure this isn't
    #  called from within a subshell
    local n_tries pid name fail n_retries=${1:-6} bash cmd args
    RANDOM=$$                   # seed RNG
    #set -x
    $v3Echo "Need Lock."
    if [ -z "$have_lock" ]; then
        $v3Echo "Acquiring Lock ---"
        if [ ! -w "${lockdir%/*}" ]; then
            needColors
            printf >&2 "${color_red}Can't lock ('%s/' not writable)${color_reset}\n" "${lockdir%/*}"
            return 1
        fi
        trap "rmLock" EXIT      # safe because it removes the lock only if it is ours
        while ! mkdir 2>/dev/null "$lockdir"; do
            pid=$(cat 2>/dev/null "$pidfile")
            if [ "$pid" ]; then
                # check if the PID still belongs to a gemplug process
                read bash cmd args <<EOF
$(tr 2>/dev/null '\000' ' ' < /proc/$pid/cmdline)
EOF
                [ "${cmd##*/}" != "$pname" ] && break # no.
            fi
            if [ $((++n_tries)) -gt "$n_retries" ]; then
                needColors
                printf >&2 "$color_red%s$color_reset\n" "Giving up on lock '$lockdir'"
                break
            fi
            while :; do
                rnd="$RANDOM"
                [ "$rnd" -gt 100 ] && break
            done
            rnd="$((${rnd:2:1}/4)).${rnd:1:1}"
            $v3Echo "Locked by another process, sleeping for $rnd secs"
            sleep "$rnd"
        done
        mkdir -p "$lockdir"     # (on dying lock)
        echo "$$" > "$pidfile"
        have_lock=1
        $v3Echo "Got Lock."
    else
        $v3Echo "Already have one"
    fi
    #set +x
    return 0
}

trap 'sispmctl_err=1' USR1
execSispmctl(){
    local n_tries=3 res
    while [ $((--n_tries)) -gt 0 ]; do
        $vCmd "$sispmctl" "$@" && break
    done
    res=$?
    [ "$res" != 0 ] && kill -USR1 $$
    return "$res"
}


assertUnique(){
    # Syntax: assertUnique string <array-name>
    #  Assserts that name (i.e. no spaces) $1 is not yet in array $2
    #  Then adds it to $2
    local name="$1"
    if eval [[ '${'$2'[*]}' != '*" $1 "*' ]]; then
        eval $2'[${#'$2'[@]}]="$1"' # insert
        return 0
    fi
    return 1
}

get_usb_pos(){
    # (This duplicates functionality which is also found elsewhere, e,g,
    # in udevinfo, and may be replaced by grepping its output once the
    # application interface there stablizes.  Until then, asking sysfs
    # directly seems safer against future changes. -2007-11-12 hcz)

    # regular expressions for the IDs we want the topological position
    # of:
    idProduct_re='^fd1[0-3]$'
    idVendor_re='^04b4$'

    # max. recursion level:
    maxlevel=6                      # hubs can be daisy chained up to 5 max.

    # pattern for trees to be traversed (a '*' will be appended):
    sys_bus_usb_devices_usb="/sys/bus/usb/devices/usb"

    traverse()(
        # Syntax: traverse cur_recursion_level cd_to
        local level="$1"
        cd "$2"
        if [[ "$(< idVendor)" =~ $idVendor_re ]] \
            && [[ "$(< idProduct)" =~ $idProduct_re ]]; then
            printf "%03d/%03d %s\n"  \
                    "$(<busnum)" "$(<devnum)" \
                    "${2##*/}"
        fi
        for dir in [0-9]*; do
            if [[ "$dir" =~ ^[-0-9.]+$ ]] && [ "$level" -le "$maxlevel" ]; then
                traverse  $((level+1)) "$dir"
            fi
        done
    )

    for dir in "$sys_bus_usb_devices_usb"*; do
        [ -d "$dir" ] && traverse 0 "$dir"
    done
}

getSispmctlS(){
    # reads in sispmctl -s into $sispmctl_S
    # Due to spurious errors reading is done multiple times
    local i sistmp err tries=0
    [ "$sispmctl_S" ] && return
    while [ $((++tries)) -le 3 ]; do
        sistmp="$sispmctl_S"
        sispmctl_S="$(execSispmctl -s)" || err=1
        if [ -n "$sistmp" ] && [ "$sistmp" != "$sispmctl_S" ]; then
            $vEcho "$pname: Inconsistent readout from 'sispmctl -s'"
            tries=0
        fi
        [ $((++tries)) -gt 10 ] && abort "Can't get a consistent answer from 'sispmctl -s' after $tries tries"
    done
    if [ "$err" ];then
        finale_furioso="$finale_furioso$_nl${color_red}Note: we had serious errors${color_reset}"
    fi
}

getGemdevArray(){
    # Calls 'sispmctl -s and fills the following arrays:
    #  gemdev_pos
    #  gemdev_bus_dev
    #  gemdev_serial
    #  gemdev_max_outlet
    #  gemdev_gembird_no
    #  gemdev_usb_pos
    # unsets gemdev_used[]
    # Same index belongs to same device.  The index itself has no
    # special meaning (currently identical to gemdev_gembird_no).
    local linepos bus_dev idx usb_traversed sistmp="-"

    getSispmctlS
    idx=-1
    while IFS= read line; do
        [[ "$line" =~ ^$_s*$ ]] && continue
        [[ "$line" =~ ^'Accessing' ]] && continue
        if [[ "$line" =~ 'Gembird #'($_n) ]]; then
            # Gembird #0
            : $((idx++))
            unset gemdev_used[idx]
            gemdev_gembird_no[idx]="${BASH_REMATCH[1]}"
        elif [[ "$line" =~ 'USB information:  bus '($_n)', device '($_n) ]]; then
            # USB information:  bus 006, device 002
            gemdev_bus_dev[idx]="${BASH_REMATCH[1]}/${BASH_REMATCH[2]}"
        elif [[ "$line" =~ 'device type:      '($_n)'-socket SiS-PM' ]]; then
            gemdev_max_outlet[idx]="${BASH_REMATCH[1]}"
        elif [[ "$line" =~ "serial number:    "($_S+) ]]; then
            # serial number:    01:01:02:0b:22
            gemdev_serial[idx]="${BASH_REMATCH[1]}"
        else
            abort "Unknown response from $sispmctl -s: '$line'"
        fi
    done <<EOF
$sispmctl_S
EOF
    usb_traversed=$(get_usb_pos)
    while read bus_dev pos; do
        unset found
        for ((idx = 0; idx < ${#gemdev_bus_dev}; idx++)); do
            if [ "${gemdev_bus_dev[idx]}" = "$bus_dev" ]; then
                gemdev_usb_pos[idx]="$pos"
            fi
        done
    done <<EOF
$usb_traversed
EOF
}

findIdx(){
    # Syntax: findIdx <array_name> <element>
    # returns on stdout the index into <array_name> of <element>, empty if none.
    local idx
    eval max_idx='"${#'$1'[@]}"'
    for ((idx = 0; idx < max_idx; idx++)); do
        if eval [ '"${'$1'[idx]}"' = '"$2"' ]; then
            echo "$idx"
            return
        fi
    done
}


# indices into the elements of a single outlets[] line (rarely used)
# index:                          position after 'set -- $outlet':
i_usbpos=0                      # $1
i_busdev=1                      # $2
i_gemno=2                       # $3
i_serial=3                      # $4
i_max_outlet_no=4               # $6
i_outlet_no=5                   # $6
i_section=6                     # $7
i_outlet_name=7                 # $8
i_aliases=8                     # $9

readRcFile(){
    # readRcFile
    #
    # interprets the rc file.
    #
    # result: outlets[]:  Each array element is a string describing 1 outlet:
    #
    #    1      2    3 4 5 6           7 ....
    # "4-2.2 004/005 01:0b:a4:c9:45 0 4 2 Peripherals Scanner ..."
    #  ^^^^^ ^^^^^^^ ^^^^^^^^^^^^^^ ^ ^   ^^^^^^^^^^^ ^^^^^^^^^^^-
    #    |      |    |              | | | |           |
    #    |      |    |              | | | |           + ---#8 [#9 ..] Outlet name and aliases
    #    |      |    |              | | | +-#7  The device's name (the [ section ])
    #    |      |    |              | | +---#6: The individual outlets' number within this device
    #    |      |    |              | +-----#5: The total number of outlets
    #    |      |    |              +-------#4: The Gembird's number for sispmctl command
    #    |      |    +----------------------#3: The Gembird's serial number
    #    |      +----------- #2: The USB Bus/device as reorted by get-usb-pos and sispmctl -s
    #    +------------------ #1: The topological pos position as reportet by get-usb-pos

    # results:
    #  outlets[]
    #  all_names: list of all encountered outlet names and aliases
    #  all_sections:   -"-                           section names
    local idx idx_new line fatal rc_file cur_gem lineno lineno_for_errmsg
    local continuation_line

    perr(){
        local skip_sect_new
        # Syntax: perr [-skip] [-fatal] text
        while [[ "$1" == -* ]]; do
            case "$1" in
                -skip) skip_sect_new=1;;
                -fatal) fatal=1;;
                *) abort "${FUNCNAME[1]}: internal error";;
            esac
            shift 1
        done
        [ "$errtext" ] && printf "%s\n" "$errtext"
        printf "  %s:\n" "$*"
        if [ "$have_sect" ]; then
            printf "  line %d, section [%s]: '%s'\n" "$lineno_for_errmsg" "$section" "$orig_line"
        else
            printf "  line %d: '%s'\n" "$lineno"  "$orig_line"
        fi
        unset errtext
        if [ "$skip_sect_new" ] && [ -z "$skip_sect" ]; then
            echo >&2 "(This error will not be reported again until configuration is changed.)"
            skip_sect=1
        fi
    } >&2

    getIdx(){
        # Syntax: getIdx idx
        # Accepts idx if new or same
        if [ "$idx" ] && [ "$idx" -ne "$1" ]; then
            echo $idx
            perr -fatal "Conflicting pos ('$pos') and serial ('$serial') statements"
        else
            idx="$1"
            gemdev_used[idx]=1
        fi
    }

    chkIdx(){
        # Syntax: chkIdx
        # barf if idx is undefined
        [ "$idx" ] && return 0
        perr -fatal -skip "need preceding outlet definition"
        return 1
    }

    # get bus mapping
    getGemdevArray

    section="1st"
    l_section=0
    l_outlet=9
    if ! findRcFile "$my_rc_file" "${rcdirs[@]}"; then
        if [ "$opt_T" ]; then
            for dir in "${rcdirs[@]}"; do
                rc_file="$dir/$rc_file"UXS
                touch 2>/dev/null "$rc_file" && break
            done \
            || abort "Cannot create 'my_rc_file' in any of: ${rcdirs[*]}"
        else
            abort "Internal: Cannot open $my_rc_file" # should have been recognized earlier
        fi
    fi
    errtext="$pname: in file '$rc_file':"
    assertUnique "ALL" all_names # enter "ALL" into empty list
    shopt -s nocasematch
    {
        while unset curline; IFS= read -r line; do
            : $((++lineno))
            lineno_for_errmsg="$lineno"
            while [[ "$line" =~ ^$_s*(.*$_S)$_s*(\\n)(\\)$ ]] \
                || [[ "$line" =~ ^$_s*(.*$_S)$_s*(\\)$ ]]; do
                # handle eol backslash continuation
                curline="${curline:+$curline }${BASH_REMATCH[1]}"
                [ "${BASH_REMATCH[3]}" ] && curline="$curline$_nl"
                IFS= read -r line
                : $((++lineno))
            done
            line="${curline:+$curline }$line"

            orig_line="$line"
            # strip comments off
            while [[ " $line" =~ ^(.*)$_c.*$ ]]; do
                $v2Echo "stripping comment: '$line' -> '${BASH_REMATCH[1]}'"
                line="${BASH_REMATCH[1]}"
            done

            # strip leading & trailing whitespace, skip (now) empty lines
            [[ "$line" =~ ^$_s*($_S+($_s+$_S+)*)$_s*$ ]] || continue # empty
            line="${BASH_REMATCH[1]}"

            if [ -z "$skip_sect" ]; then
                if [[ "$line" =~ ^([[:alpha:]][-[:alnum:]]+)$_s*=$_s*($_S+($_s+$_S+)*)$_s*$ ]]; then
                    config_item="${BASH_REMATCH[1]}"
                    arg="${BASH_REMATCH[2]}"
                    case "$config_item" in
                        pos)
                       # found "pos = 4-2.2"
                            [ "$pos" ] && perr -fatal "duplicate pos statement in section"
                            idx_new=$(findIdx gemdev_usb_pos "$arg")
                            [ "$idx_new" ] || perr -skip "There's no Gembird at USB position '$arg'"
                            assertUnique "$arg" all_pos || perr -fatal "Duplicate position"
                            getIdx "$idx_new"
                            $v2Echo "parse: pos: '$line' -> $pos -> $idx"
                            ;;
                        serial)
                      # found "serial = 01:ffffff9a:ffffffa9:ffffffdb:41
                            [ "$serial" ] \
                                && perr -fatal "duplicate serial statement in section [ $section ]"
                            idx_new=$(findIdx gemdev_serial "$arg")
                            [ "$idx_new" ] || perr -skip "Unknown serial '$serial'"
                            assertUnique "$arg" all_serials || perr -fatal "Duplicate serial number"
                            getIdx "$idx_new"
                            $v2Echo "parse: serial: '$line' -> $serial -> $idx"
                            ;;
                        path|pre-on|pre-off|post-on|post-off)
                            chkIdx
                            cur_gem="${gemdev_gembird_no[idx]}"
                            var="gemdev_${cur_gem}_${config_item//-/_}"
                            eval $var[outlet_no]='"$arg"'
                            $v2Echo "parse: $config_item: '$line' -> $var[$outlet_no]='$arg'"
                            ;;
                        *)
                            perr -fatal "Unknown statement '$config_item'"
                            ;;
                    esac
                elif [[ "$line" =~ ^([0-9])$_s*=$_s*(($_S+)($_s+$_S+)*)$_s*$ ]]; then
                # found "3 = Laser OKI"
                    len="${#BASH_REMATCH[3]}" # outlet name len
                    outlet_no=${BASH_REMATCH[1]}
                    names=${BASH_REMATCH[2]}
                    for name in $names; do
                        assertUnique "$name" all_names || perr -fatal "Duplicate outlet name '$name'"
                    done
                    if chkIdx; then
                        if [ " ${gemdev_max_outlet[idx]}" -lt "$outlet_no" ]; then
                            skip_sect=1
                            perr -fatal "Outlet number is $outlet_no but only $n_outlets outlets are available"
                            continue
                        fi
                        [ "$len" -gt "$l_outlet" ] && l_outlet=$len # max outlet name len
                        cur_outlet="${gemdev_usb_pos[idx]} ${gemdev_bus_dev[idx]} ${gemdev_gembird_no[idx]} ${gemdev_serial[idx]} ${gemdev_max_outlet[idx]} $outlet_no $section $names"
                        outlets[${#outlets[@]}]="$cur_outlet"
                        cur_gem="${gemdev_gembird_no[idx]}"
                        #eval 'gemdev_'${cur_gem}'_path['$outlet_no"]="
                        $v2Echo "parse: outlet: '$line' -> $cur_outlet"
                    fi
                elif [ -z "$skip_sect" ]; then
                    barf=yes
                fi
                [ "$skip_sect" ] && echo >&2 "Skipping the rest of the offending '[ $section ]' section."
            fi   # if [ -z $skip_sect ]
            if [[ "$line" =~ ^'['$_s*($_S+)$_s*']'$ ]]; then
                # found "[ Peripherals ]"
                section="${BASH_REMATCH[1]}"
                len=${#section}
                [ "$len" -gt "$l_section" ] && l_section=$len
                have_sect=1
                assertUnique "$section" all_sections || perr -fatal "Duplicate section"
                unset skip_sect idx pos serial barf cur_outlet
                $v2Echo "parse: section: '$line' -> '$section'"
            fi
            if [ "$barf" ]; then
                perr -fatal "Not a valid line"
                abort "Errors in configuration file"
            fi
        done
        [ "$fatal" ] && abort "Errors in configuration file, Aborting."
    } < "$rc_file"

    [ -z "$errtext" ] && echo >&2
    # Now check if there are any Gembirds left out:
    for ((idx=0; idx < "${#gemdev_usb_pos[@]}"; idx++)); do
        if [ "${gemdev_used[idx]}" != "1" ]; then
            printf >&2 "\
* Note: unconfigured Gembird found at bus/device %s
    pos = %s
    serial = %s\n" \
    "${gemdev_bus_dev[idx]}" "${gemdev_usb_pos[idx]}" "${gemdev_serial[idx]}"
        fi
    done
}

template_hdr="\
# Configuration template for $my_name
#
# Created $(date -Imin) for $USER ${HOSTNAME:+on $HOSTNAME}

# This file describes the mapping from individual outlet names of
# Gembird USB-conrolled power switches to the position of their
# devices on the USB and/or to their serial number.

### Some syntax rules:
#
#   Comments
#       '#' and ':' at the start of a word (i.e. after white space or a
#       newline) start a comment.  All text up to the end of the line will be
#       ignored.
#
#   Case
#       All matching is done without regard to case (case-insensitive).
#
#   Names (outlet and Gembird device identifiers)
#       Names can be made of any non-whitespace characters.  They are allowed
#       to include anything but the following:
#        - They cannot start with a comment character ('#', ';')
#        - White space cannot be embedded as it is a delimiter,
#        - In device (=section) names '[' and ']' cannot be used (delimiters)
#
#   Indenting
#       Indenting is not mandatory; leading white space is always ignored.  Use
#       it as you like, In all places where white space is allowed/required,
#       any amount can be used.
#
#   Key elements
#       All the descriptions of the key elements below are preceded by an
#       example line demonstrating their use.
#
#       [ Gebird_name ]
#            Starts a device section, giving the device and the section the name
#            within the brackets.  Any white space between brackets and device
#            name is optional.
#
#        pos = 3.1
#            Names the topological position of the device on the USB as reported
#            by gemplug -d <one-of-its-outlets>.
#
#        serial = 01:02:03:04:05
#            Names the Gembird's serial number as reported by gemplug -d <one-
#            of-its-outlets> or sispmctl -s.
#
#        1 = Outlet-name [Alias ...]
#            Number, '=', Name Aliases ...  names the individual plug outlets.
#            It can only be used if at least one of pos or serial are given
#            before and in the same section.  If both are given, they are
#            required to match the same device.  Outlet numbers (the numbers in
#            the first column) count from number 1 upwards per device.

###  An example for a device entry:
#
# [ Peripherals ]
#    pos = 4-2.2
#    1 = Scanner
#    2 = Printer Laser
#    3 = DVB-T DVBT Telly
#    4 = Backup1 ICY ext_ATA


# Here we go:

"

addOneTemplate(){
    # Syntax: addOneTemplate <index into gemdev_*>
    while :; do
        sect="Gembird-$((++template_section_idx))"
        assertUnique "$sect" all_sections && break;
    done
    printf "\n[ $sect ]\n" "$1"
    printf "  pos = %s      # currently at bus/device %s\n" "${gemdev_usb_pos[$1]}" "${gemdev_bus_dev[$1]}"
    printf "  serial = %s   # you may remove either pos or serial\n" "${gemdev_serial[$1]}"
    for ((outlet=1; outlet <=  "${gemdev_max_outlet[$1]}"; outlet++)); do
        while :; do
            out="OUT-$((template_out_idx++))"
            assertUnique "$out" all_names && break;
        done
        printf "  %d = %s\n" "$outlet" "$out"
    done
}

addTemplate(){
    # Syntax: addTemplate <index into gemdev_*>
    # Adds a template.  Creates rc file if needed.
    local touch_result idx written need_update
    getGemdevArray
    echo >&2 "Checking current configuration ..."
    readRcFile
    for idx in ${!gemdev_usb_pos[@]}; do
        if [ "${gemdev_used[idx]}" != 1 ]; then
            need_update=1
            break
        fi
    done
    if [ -z "$need_update" ]; then
        echo >&2 "Nothing to write (no unconfigured Gembird found)."
        return
    fi
    if [ "$opt_W" ]; then
        if ! findRcFile "$my_rc_file" "${rcdirs[@]}"; then
            for rc_dir in "${rcdirs[@]}"; do
                rc_file="$rc_dir/$my_rc_file"
                touch_result="$touch_result$_nl$(touch 2>&1 "$rc_file")" && break
            done
            if [ $? != 0 ]; then
                echo "$touch_result"
                abort "Cannot create configuration file"
            fi
            echo "$template_hdr" > "$rc_file" || abort "Cannot write template"
        fi
        exec >> "$rc_file" || abort "Can't write template file '$rc_file'"
    fi
    for idx in ${!gemdev_usb_pos[@]}; do
        if [ "${gemdev_used[idx]}" != 1 ]; then
            [ "$opt_W" ] && printf >&2 "Writing template for unconfigured Gembird at %s\n"\
                             "${gemdev_bus_dev[idx]}"
            addOneTemplate "$idx"
        fi
    done
    if [ "$opt_W" ]; then
        echo >&2 "
Configuration  written to '$rc_file'.
You may now want to edit it and give the items there meaningful names."
    fi

}


findRcFile(){
    # Syntax: findRcFile <fnam> <dir1> <dir2> ....
    # returns first file <fnam> from dirs which is readable
    # Result in $rc_file (empty if none), true/false
    local rc_dir fnam="$1"
    shift
    for rc_dir in "$@" ""; do
        rc_file="$rc_dir/$fnam"
        [ -r "$rc_file" ] && [ -f "$rc_file" ] && return
    done
    return 1
}

findRcDir_RW(){
    # Syntax: findRcFile <path1> <path2> ....
    # returns first dir from "$@" which is r/w-able
    # Result in $rc_dir (empty if none), true/false
    for rc_dir in "$@" ""; do
        [ -w "$rc_dir" ] && [ -r "$rc_dir" ] && [ -d "$rc_dir" ] && return
    done
    return 1
}

initialize(){
    local compiled_rc need_update rc_file rc_dir

    # Find first rc file along $rcdirs[]
    if findRcFile "$my_compiled_rc_file" "${rcdirs[@]}"; then  # set rc_file
        compiled_conf="$rc_file"
    else
        need_update=1           # none
    fi

    # find first directory which is writable along rcdirs[]
    # first, try directory of current rc
    rc_dir="${rc_file%/*}"
    if [ -z "$rc_dir" ] || [ ! -w "$rc_dir" ]; then
        # we don't have write permissions in the rc file's directory
        if ! findRcDir_RW "${rcdirs[@]}"; then
                # we have to create our rc directory
            for rc_dir in "${rcdirs[@]}"; do
                md_result="$md_result$_nl$(mkdir -m 0755 -p "$rc_dir" 2>&1)" && break
            done
            if [ $? -ne 0 ]; then
                echo "$md_result"
                abort "Cannot write compiled configuration file"
            fi
        fi
    fi
    compiled_rc="$rc_dir/$my_compiled_rc_file"
    last_sispmctl_S="$rc_dir/$last_sispmctl_S"

    # check if sispmctl -s reports a different config than last time

    getSispmctlS
    if [ ! -r "$last_sispmctl_S" ] || [ "$version$_nl$sispmctl_S" != "$(<"$last_sispmctl_S")" ]; then
        need_update=1
        echo "$version$_nl$sispmctl_S" > "$last_sispmctl_S"
    fi


    if ! findRcFile "$my_rc_file" "${rcdirs[@]}"; then  # set rc_file
        # None found.
        echo "No configuration file found.  This is where I've looked for one:"
        for rc_dir in "${rcdirs[@]}"; do
            echo " $rc_dir/$my_rc_file"
        done
        echo "You may try again using the -T or -TW option to create a template."
        abort "Can't proceed."
    fi

    # re-compute outlets[] if needed
    if [ "$need_update$opt_f" ] \
        || [ "$rc_file" -nt "$compiled_conf" ]; then

        $v2Echo "Parsing '$rc_file'"
        readRcFile

        if [ -z "$sispmctl_err" ]; then
            if [ -e "$compiled_rc" ]; then
                chmod 0600 "$compiled_rc" || abort "Can't set permissions of '$compiled_rc'"
            fi
            printf > "$compiled_rc" "%s\n" "\
# Compiled configuration, generated by $pname at $(date '+%Y-%m-%d %H:%M')
#                       *** DO NOT EDIT ***"
            declare -p outlets l_section l_outlet ${!gemdev_*} >> "$compiled_rc"
            getColors
            colors_TERM="$TERM"
            declare -p ${!color_*} colors_TERM have_colors >> "$compiled_rc"
            $vEcho "$compiled_rc written."
            chmod 0400 "$compiled_rc" || abort "Can't set permissions of '$compiled_rc'"
        else
            $vEcho "$compiled_rc NOT written because of sispmctl errors"
        fi
    fi
}


getCurDevStates(){
    # Syntax: getCurDevStates [-f] <sis_dev>
    # opt: -f: force read (ignore cached data)
    # Result in cur_dev_states[] = (sisdev, state0, state 1, ..)
    local path idx i path_array sispm_result sispm_old i err=0

    if [ "$1" = "-f" ]; then
        shift
        unset states_cache[sisdev] cur_dev_states
    else
        [ "$1" -eq "${cur_dev_states[0]:--1}" ] && return
    fi
    sisdev="$1"
    if [ -z "${states_cache[sisdev]}" ]; then
        while [ $((++i)) -lt 2 ]; do # -lt 2: agrees 3 times: 0, 1, 2
            [ "$err" -gt 10 ] && abort "Can't read Gembird #$sisdev"
            sispm_old="$sispm_result"
            sispm_result="$(execSispmctl -n -q -d $sisdev -g all)" || : $((++err))
            [ "$err" -gt 20 ] && abort "Can't get a consistent readout from '$sispmctl -n -q -d $sisdev -g all' after $err tries"
            if [ -n "$sispm_old" ] && [ "$sispm_result" != "$sispm_old" ]; then
                $vEcho "Unstable readout (was: '"$sispm_old"', is: '"$sispm_result"')from command '$sispmctl -n -q -d $sisdev -g all'"
                : $((++err))
                i=0
            fi
        done
        states_cache[sisdev]="$1 $sispm_result"
        set -- ${states_cache[sisdev]}
        shift
        # Notify for external program(s)
        eval 'path_array=( "${'gemdev_${sisdev}_path'[@]}" )'
        for ((i=0; i < "${#path_array[@]}"; i++)); do
            path="${path_array[i]}"
            if [ "$path" ]; then
                mkdir -p "${path%/*}"
                if [ "$1" != "$(< "$path")" ]; then
                    echo "$1" > "$path"
                    touch "$path.chgd"
                    echo $(($(<"$path.chgd") + 1)) > "$path.chgd"
                fi
            fi
            shift
        done
    fi
    cur_dev_states=(${states_cache[sisdev]})
}

findOutletByName(){
    # Sytnax: findPlugByName <Start_of_name>
    # Returns: line from outlets[] in stdout
    local res srch="$1" found
    shopt -s nocasematch            # from now on we're case insensitive
    for outlet in "${outlets[@]}"; do
        set -- $outlet
        shift 7                 # skip to 1st name
        while [ "$1" ]; do
            case "$1" in
                $srch*)
                    res="$outlet"
                    found[${#found[@]}]="$1"
                    break
                    ;;
            esac
            shift
        done
    done
    if [ "${#found[@]}" -gt 2 ]; then
        abort "Ambiguous matches for '$srch': ${found[*]}"
    elif [ -z "$res" ]; then
        abort "No match for '$srch'"
    fi
    echo "$res"
}

getState(){
    # Sytax: getState [-noabort] <text>
    # returns 0 or 1 on stdout
    # with 2 Arguments, returns empty string on error, else aborts
    case "${2:-$1}" in
        0|off) echo "0";;
        1|on) echo "1";;
        *) [ "$#" != 2 ] && abort "Can't set to state '$1'. Valid states are: 0, off, 1, on";;
    esac
}

setOutlet(){
    # Syntax: setOutlet "<outlet line from outlets[]>" state [recursion_level}
    local outlet="$1" state="$2" recursion_level="${3:-0}" t_echo dev port cmd on_off dev_name

    execPrePost(){
        # Syntax: execPrePost pre|post
        eval cmd='${gemdev_'${dev}_$1_${on_off}'[port]}'
        if [ "$cmd" ]; then
            if ! $t_echo "${SHELL:-/bin/sh}" -c "$set_x$cmd" "${sh_args[@]}"; then
                echo "$1-$on_off command: '$cmd'"
                abort "$1-$on_off for outlet '$dev_name' failed"
            fi
        fi
    }

    set -- $1
    dev="$3"
    port="$6"
    dev_name="$8"
    case "$(getState "$state")" in
        0) st="0"; opt="-f"; on_off="off";;
        1) st="1"; opt="-o"; on_off="on";;
    esac
    [ "$opt_X" ] && set_x="set -x; "
    if [ "$opt_t" ]; then
        t_echo="echo not executed (-t):"
    else
        sh_args=(${my_name}-sh "$@")
    fi
    execPrePost pre
    if [ "$opt_t" ]; then
        $t_echo "Outlet '$8' set to '$on_off"
    else
        execSispmctl -q -d "$3" $opt "$6"
    fi
    getCurDevStates -f "$3"
    if [ "${cur_dev_states[$6]}" != "$st" ]; then
        if [ "$recursion_level" -lt 3 ]; then
            $vEcho "$pname: State not reported back.  Trying again."
            setOutlet "$outlet" "$state" $((recursion_level+1))
        else
            abort "Gembird device '$7', outlet '$8' doesn't report back the new setting after $recursion_level retries"
        fi
    fi
    execPrePost post
}

getOutlet(){
    # Syntax: getOutlet <outlet-line>
    # returns state in $state
    local idx cur=($1)
    getCurDevStates "${cur[i_gemno]}"
    state="${cur_dev_states[${cur[i_outlet_no]}]}"
}

dumpOutlet(){
    # Syntax: dumpOutlet <outlet-line>

    local idx cur=($1) line
    getOutlet "$1"
    [ "$state" -eq 0 ] && state="0 (Off)" || state="1 (on)"
    echo
    printf "Outlet name:       %s\n" "${cur[i_outlet_name]}"

    printf "Alias(es):        "
    for ((idx=i_aliases; idx < ${#cur[@]}; idx++)); do
        printf " %s" "${cur[idx]}"
    done
    echo

    printf "USB position:      %s\n" "${cur[i_usbpos]}"
    printf "Bus/Device:        %s\n" "${cur[i_busdev]}"
    printf "Gembird#:          %s\n" "${cur[i_gemno]}"
    printf "Serial #:          %s\n" "${cur[i_serial]}"
    printf "Number of outlets: %s\n" "${cur[i_max_outlet_no]}"
    printf "outlet #:          %s\n" "${cur[i_outlet_no]}"
    printf "Section name:      [ %s ]\n" "${cur[i_section]}"
    stat --printf "%A\n%a\n%U\n%u\n%G\n%g\n$'\x04'" /dev/bus/usb/"${cur[i_busdev]}" \
    | (
        IFS="$_nl" read -d  $'\x04' access access_octal  owner owner_id group group_id
        if [[ "$access_octal" =~ ^..[67]$ ]]; then
            printf "Access rights:     %s (%s)\n" "World read and write permissions" "$access"
        else
            printf "Access rights:     %s\n" "$access"
                printf "  World:           no write permission\n"
            if [[ "$access_octal" =~ ^.[67].$ ]]; then
                printf "  Group:           %s (read and write permissions)\n" "$group"
            elif ! [[ "$access_octal" =~ ^.[67].$ ]]; then
                printf "  Group:           %s (no write permission)\n"  "$group"
            fi
            if [[ "$access_octal" =~ ^[67]..$ ]]; then
                printf "  Owner:           %s (read and write permissions)\n" "$owner"
            else
                printf "  Owner:           %s (no write permission)\n" "$owner"
            fi
        fi
    )
    printf "current state:     %s\n" "$state"

}


listOneOutlet(){
    # Syntax: listOneOutlet "<outlet line from outlets[]>"
    local r state
    if [ "$opt_g" ]; then
        getOutlet "$1"
        [ "$state" -eq 1 ]
        exit
    elif [ "$opt_d" ]; then
        dumpOutlet "$outlet"
    else
        if [ -z "$opt_qq" ]; then
            # State
            getOutlet "$1"
            needColors
            if [ "$state" = "1" ]; then
                [ "$opt_n" ] || state="(On)"
                printf "%s" "$color_green_bg$color_bold_grey"
            else
                [ "$opt_n" ] || state="(Off)"
                printf "%s" "$color_red_bg$color_bold_grey"
            fi
            printf "%-5s" "$state"
            printf "%s " "$color_reset"
        fi
        set -- $1
        # section
        [ -n "$opt_v" ] && printf "%-*s " "$l_section" "$7"
         # Name
        ( [ "$opt_qq" ] || [ -z "$opt_q" ] ) && printf "%s" "$8"
        # Aliases
        shift $(min 8 $#)
        ( [ -z "$opt_qq" ] && [ -z "$opt_q" ]) && printf " %s" "$*"
        # finale
        echo
    fi
}

listOutlets(){
    local cur_sisdev=-1
    for outlet in "${outlets[@]}"; do
        listOneOutlet "$outlet"
    done
}


############### Jobs and the like

getIntervalSpec(){
    # Syntax: getIntervalSpec "<timespec>"
    # interpret $1 as time interval (duration) starting from $cur_sec
    # (date in s since epoch, which must be defined).
    # sets global "cur_date" to a time strimg suitable for feeding to the "at" command.
    # sets global 'cur_sec' to reflect the new date (in secs since epoch)
    local days=0 hrs=0 min=0 target
    [ "$cur_sec" ] || cur_sec=$(date '+%s') || abort "Ooops: can't get current date/time"
    if ! [[ "$1" =~ [hmd:] ]]; then
        abort "'$1': need a unit specification (d[ays], h[our], m[in], ':')"
    elif ! [[ "$1" =~ $_n ]]; then
        abort "'$1': need a number in interval specification"
    elif [[ "$1" =~ ^$_s*(($_n)$_s*$_d)?$_s*(($_n)(($_s*$_h$_s*)|$_s|:))?(:?($_n)$_s*($_m)?)?$_s*$ ]]; then
        # declare -p BASH_REMATCH
        days="10#${BASH_REMATCH[2]:-0}"
        hrs="10#${BASH_REMATCH[6]:-0}"
        min="10#${BASH_REMATCH[13]:-0}"
    else
        abort "Can't interpret '$1' as a time interval"
    fi
    target=$(( ((days * 24 + hrs) * 60 + min) * 60))
    : $((cur_sec += target + 30))
    cur_date=$(date -d "1970-01-01 00:00:00 UTC + $cur_sec sec" '+%H:%M %Y-%m-%d')
    [ "$opt_v" ] && printf "time interval '%s' -> %d days %d:%d, target date: %s\n" \
        "$1" "$((days))" "$((hrs))" "$((min))" "$cur_date"
}

getTimestampSpec(){
    # Syntax: getIntervalSpec "<datespec>"
    # interpret $1 as time stamp (date)
    # sets global "cur_date" to a time strimg suitable for feeding to the "at" command.
    # sets global 'cur_sec' to reflect the new date (in secs since epoch)
    local target
    target=$(date -d "$1" "+%s") || abort "Invalid date/time specification: '$1'"
    if [ "$target" -lt "$now" ]; then
        : $((target += 24 * 3600))
        if [ "$target"  -gt "$now" ]; then
            [ "$opt_v" ] && echo "Time in the past - using tomorrow's time"
        else
            abort "date in the past: '$1'"
        fi
    fi
    cur_sec="$target"
    if [ "$1" = "now" ]; then
        cur_date="now"
    else
        cur_date=$(date -d "1970-01-01 00:00:00 UTC + $target sec" '+%H:%M %Y-%m-%d')
    fi
}

getJobs(){
    # Syntax: getJobs [<outlet>]
    # returns on stdout "jobno: date - command"
    local srch err res
    if [ "$1" ]; then
        set -- $outlet
        srch="$8"
    else
        srch=".*"
    fi
    LC_ALL=C atq -q "$at_queue" \
    | while read jobno time; do
         time="${time% $at_queue *}" # remove queue indicator and user name
         res=$(
             LC_ALL=C at -q S -c "$jobno" | sed -n '/^cd /,$'"{/^[^ ]*$my_name $srch /p}"
             [ "${PIPESTATUS[0]}" -ne 0 ] && abort "Error reading job $jobno"
         )
         [ "$res" ] && printf "%s: %s - %s\n" "$jobno" "$time" "$res"
      done
    [ "${PIPESTATUS[0]}" -ne 0 ] && abort "Error reading at-queue"
}

listJobs(){
    # Syntax: listJobs [<outlet>]
    getJobs "$1" | sed 's/#.*$//'
}

num='([0-9]+)'
fillPending_jobs(){
    # Syntax: fillPending_jobs [<outlet>]
    # fills the pending_jobs_* arrays
    local joblist idx

    joblist="$(getJobs "$1")"
    idx="${#pending_jobs_job[@]}" # add if there's already somthing inside
    while IFS= read line; do
        [ "$line" ] || break
        if [[ "$line" =~ ^$num:" "+.+" - ".*"$my_name "([^ ]+)" "*([01])" "*#" "*now=$num" "+at=$num ]]; then
            #declare -p BASH_REMATCH
            pending_jobs_job[idx]="${BASH_REMATCH[1]}"
            pending_jobs_date[idx]="${BASH_REMATCH[2]}"
            pending_jobs_device[idx]="${BASH_REMATCH[3]}"
            pending_jobs_state[idx]="${BASH_REMATCH[4]}"
            pending_jobs_now_sec[idx]="${BASH_REMATCH[5]}"
            pending_jobs_at_sec[idx]="${BASH_REMATCH[6]}"
            : $((idx++))
            have_pending_jobs=$idx
        else
            printf "Unknown job spec: '%s'\n" "$line"
        fi
    done <<EOF
$joblist
EOF
    #[ "$have_pending_jobs" ] && declare -p ${!pending_jobs_*}
}

killPending_jobs(){
    # Syntax: killPendingJobs
    # removes all jobs currently on pending_jobs_job[]
    local n_jobs job_S
    n_jobs="${#pending_jobs_job[@]}"
    if [ "$n_jobs" -gt 0 ]; then
        job_S="jobs"; [ "$n_jobs" -eq 1 ] && job_S="job"
        if [ "$opt_t" ]; then
            echo "Not removing $job_S jobs (because of -t option)."
        else
            [ "$opt_q" ] || printf "removing %d %s\n"  "$n_jobs" "$job_S"
            atrm "${pending_jobs_job[@]}"
        fi
        unset ${!pending_jobs_*}
    else
        [ "$opt_q" ] || printf "no jobs in queue.\n"
    fi
}

execAtCmds(){
    local idx result cmd at_cmd date
    $v2Echo "Installing schedule"
    for ((idx=0; idx < ${#at_registered_dates[@]}; idx++)); do
        date="${at_registered_dates[idx]}"
        cmd="${at_registered_cmds[idx]}"
        if [ "$date" = "now" ]; then
            # 'at now' sometimes enqueues commands instead of executing them now
            # so we better run the command on our own:
            cmd="${cmd/ / -L }" # dont't aquire lock
            ${cmd%#*}
        else
            at_cmd=(at -q "$at_queue" "${at_registered_dates[idx]}")
            result=$(
                echo "${at_registered_cmds[idx]}  # now=$now at=$cur_sec" \
                    | LC_ALL=C "${at_cmd[@]}" 2>&1
                declare -p PIPESTATUS
                )
            if ! [[ "$result" =~ "declare -a PIPESTATUS='([0]=\"0\" [1]=\"0\")" ]]; then
                echo "$result"
                abort "command '${at_cmd[*]}' failed."
            fi
        fi
    done
}

registerAtCmd(){
    # Syntax: execAt "<date>" "<date in s since epoch>" "<outlet line>" "0|1"
    local date="$1" date_sec="$2" state="$4" cmd
    set -- $3
    cmd="${my_path}${my_path:+/}$my_name $8 $state # now=$now at=$date_sec"
    idx=${#at_registered_dates[@]}
    at_registered_dates[idx]="$date"
    at_registered_cmds[idx]="$cmd"
    [ "$opt_q" ] || echo "scheduling: $date: ${cmd%#*}"
}

assembleAt(){
    # Syntax: assembleAt "<DateSpec>" "0|1" "<outlet line>"
    getTimestampSpec "$1"
    registerAtCmd "$cur_date" "$cur_sec" "$3" "$2"
}

assembleAfter(){
    # Syntax: assembleAfter "<TimeSpec>" "0|1" "<outlet line>"
    getIntervalSpec "$1"
    registerAtCmd "$cur_date" "$cur_sec" "$3" "$2"
}

runInterpreter(){
    # Syntax: doInterpret "$@"
    target_state="$(getState "$2")"
    now=$(date '+%s') || abort "Oops: can't get current date/time"
    fillPending_jobs "$outlet"
    if [ "$have_pending_jobs" ] && [ -z "$opt_a" ]; then
        (
            summary="are $have_pending_jobs pending jobs"
            [ "$have_pending_jobs" -eq 1 ] && summary="is 1 pending job"
            set -- $outlet
            if [ "$opt_t" ]; then
                [ -z "$opt_a$opt_r" ] \
                    && printf "Note: there $summary for '$8'.
You'll need to specify either -a or -r when you remove the -t (test) option.\n"
            else
                abort "There $summary for '$8'.
Either choose '-r' to replace them or '-a' to add new jobs."
            fi
        )
    fi
    shift 2
    if [ "$1" = "-e" ]; then
        shift
        $v2Echo "$pname: Setting '$outlet' to $target_state"
        setOutlet "$outlet" "$target_state"
        rmLock
        $vEcho "$pname: Running '$*'"
        "$@"
        res=$?
        if [ "$res" -ne 0 ]; then
            printf "%s: %s exited with error code %d\n" "$pname" "$1" "$res"
        fi
        [ "$target_state" = 0 ] && target_state=1 || target_state=0
        $v2Echo "$pname: Setting '$outlet' to $target_state"
        needLock
        setOutlet "$outlet" "$target_state"
    else
        while [ "$#" -gt 0 ]; do
            if [ -z "$cur_sec" ]; then
            # no previous action.  Check if we have to switch now,
                case "$1" in
                    for|until) assembleAt "now" "$target_state" "$outlet";;
                esac
            fi
            case "$1" in
                at) assembleAt "$2" "$target_state" "$outlet"; shift 2;;
                after) assembleAfter "$2" "$target_state" "$outlet"; shift 2;;
                until) assembleAt "$2" "$(( ! target_state))" "$outlet"; shift 2;;
                for) assembleAfter "$2" "$(( ! target_state))" "$outlet"; shift 2;;
                *) abort "Unknown element on command line: '$1'";;
            esac
         done

         # If we've made it here, we now can execute our 'at' commands:
         if [ "$opt_t" ]; then
             [ "$opt_q" ] || echo "Scheduled jobs not activated (-t)"
         else
             execAtCmds
         fi
    fi
}

################ misc

version(){
    echo "\
$version
(C) 2008 Heike C. Zimmerer
This software is licensed under the GNU Public License (GPL), version 3."
}



makeWorldReadWritable(){
    local bus_dev
    for bus_dev in "${gemdev_bus_dev[@]}"; do
        chmod a+rw "$dev_bus_usb/$bus_dev" || abort "Can't set permissions"
    done
    echo "$pname: Permissions done."
}


execMode(){
    # Syntax: execMode <mode(char)> "$@"
    # Execute special modes
    mode="$1"
    shift
    case "$mode" in
        C) makeWorldReadWritable ;;
        T) addTemplate 0;;
        m)  # gemplug -m outlet outlet ... state
            args=("$@")
            while [ "$2" ]; do
                outs[${#outs[@]}]=$(findOutletByName "$1")
                shift
            done                # don't exec anything in case of error
            for outlet in "${outs[@]}"; do
                setOutlet "$outlet" "$1"
            done
            ;;
        p)
            for jobno; do
                if atrm "$jobno"; then
                    echo "job $jobno purged"
                else
                    err=1
                fi
            done
            [ "$err" ] && abort "Not all jobs could be removed."
            ;;
        l|d)
            [ "$#" -eq 0 ] && listOutlets
            for dev; do
                outlet=$(findOutletByName "$dev")
                listOneOutlet "$outlet"
            done
            ;;
        k)
            [ "$#" -lt 1 ] && abort "'-k': Not enough arguments"
            if [ "$1" = "ALL" ]; then
                fillPending_jobs
            else
                for dev; do
                    outlet=$(findOutletByName "$dev")
                    fillPending_jobs "$outlet"
                done
            fi
            killPending_jobs
            ;;
        j)
            [ "$#" -eq 0 ] && listJobs
            for dev; do
                outlet=$(findOutletByName "$dev")
                listJobs "$outlet"
            done
            ;;
    esac
}

doGetopts(){
    while getopts vx-:lhrtnqjkidarcygfLCTWpXm argv; do
        case "$argv" in
            p|j|k|l|d|i|C|T|m)
                [ "$mode" ] && abort "'-$argv' and '-$mode' are mutually exclusive options"
                mode="$argv";;
        esac
        case "$argv" in
            d|p|f|g|t|n|l|m|j|k|a|r|c|i|y|C|T|W|X|L) eval opt_$argv=1;;
            x) set -x;;
            q) [ "$opt_q" ] && opt_qq=1; opt_q=1;;
            v)
                case $((++opt_v)) in
                    1) vEcho="echo";;
                    2) v2Echo="echo :+ "; vCmd=vCmd;;
                    3) v3Echo="echo :* ";;
                esac
                ;;
            -*)
                case "$OPTARG" in
                    li*) version; echo "$LICENSE"; exit 0;;
                    c*) doGetopts "C";;
                    t*) doGetopts "T";;
                    V*) echo "$time_stamp"; version; exit;;
                    v*) version; exit;;
                    h*) usage;;
                    *) echo "unknown long option '--$OPTARG'"; exit 127;;
                esac
                ;;
            *) usage;;
        esac
    done
}

###################### main

pname=${0##*/}
[ -t 1 ] && have_term=1
export HOME=${HOME:-/$default_HOME}
unset IFS
# get command line options
vEcho=:
v2Echo=:
v3Echo=:
cmdline=("$0" "$@")
doGetopts "$@"
shift $((OPTIND-1))
cmdargs=("$@")


# Templates need to be written before initialization
if [ "$mode" = 'T' ]; then
    addTemplate
    exit
fi

####  Initialize: get configuration (assemble it if required)

# Make sure core variables are clean at this point:
unset outlets l_section l_outlet ${!gemdev_*} ${!color_*} colors_TERM have_colors

# get an exclusive lock for sispmctl and our files
[ "$opt_L" ] || needLock 10

# Assemble core variables from scrath if required.  If not, read them
# in from saved state:
initialize
[ "$outlets" ] || . "$compiled_conf"

# clean out old colors it $TERM has changed or none are required:
if [ "$colors_TERM" != "$TERM" ] \
   || [ "$opt_c" ] \
   || [ -z "$have_term" ]; then
    unset ${!color_*} have_colors
fi

### Now go do some useful work:
if [ "$mode" ]; then
    # 'mode' commands differ in syntax, so they are recognized and
    # treated separately:
    execMode "$mode" "$@"
else
    # standard syntax: [outlet [state [commands ...]]]
    [ "$#" -ge 1 ] && outlet=$(findOutletByName "$1")
    if [ "$opt_r" ]; then
        fillPending_jobs "$outlet"
        killPending_jobs
        unset ${!pending_jobs_*} have_pending_jobs
    fi

    case "$#" in
        0) listOutlets;;
        1) listOneOutlet "$outlet";;
        2) setOutlet "$outlet" "$2";;
        *) runInterpreter "$@";;
    esac
fi
[ -n "$sispmctl_err" ] && echo "$pname: sispmctl errors."
[ -n "$finale_furioso" ] && echo "$finale_furioso"
exit 0
