# $Id$
"""
wibo host utility

Usage:
    python wibo_host.py [OPTIONS]

    Options:
      -P PORT : Port string ("port" or "port:baudrate"), e.g. /dev/ttyS0, COM1:38400
                [default COM2:38400]
      -h      : show help and exit
      -V      : show version and exit
      -A ADDR : set current node addresses, e.g. -A 1, -A 1,3,5 or -A 1:10,15
                [default 1:8]
      -u FILE : selectively update nodes selected by ADDR with FILE
      -U FILE : broadcast update nodes with FILE
      -S      : scan for nodes in range min(ADDR):max(ADDR),
      -J      : send jump_to_bootloader frame to nodes selected by ADDR
      -E      : send exit_from_bootloader frame to nodes selected by ADDR
      -c CHANS: issue jump bootloader over the given channels, default: [11]
      -v      : increase verbose level

      Examples:

"""
import serial, string, re, time, sys, getopt
HISTORY = "wibohost.hist"
VERSION = 0.01
PORT = "COM2"
BAUDRATE = 38400
ADDRESSES = [1,2,3,4,5,6,7,8]
WHOST = None
VERBOSE = 0
BL_CHANNEL = 11
CHANNELS = [11]
DELAY = 0.02

class WIBORxDevice(serial.Serial):
    """ Basic class to interface an WIBO Host on serial line """
    CNT = 0
    def __init__(self, *args, **kwargs):
        serial.Serial.__init__(self, *args, **kwargs)
        self.flt = re.compile("(?P<code>[A-Z]+)(?P<data>.*)")
        self.VERBOSE = 0

    def _flush(self):
        self.read(self.inWaiting())

    def _sendcommand(self, cmd, *args):
        self.CNT += 1
        self._flush()
        cmd = string.join(map(str,[cmd]+list(args)))
        if self.VERBOSE > 2:
            print "TX[%d]: %s" % (self.CNT, cmd)
        self.write(cmd + '\n')
        # TODO evaluate returning line and parse for parameters
        s = self.readline().strip()
        if self.VERBOSE > 2:
            print "RX[%d]: %s" % (self.CNT, s)
        m = self.flt.match(s)
        if m == None:
            return dict(code = "NO RESPONSE", data = cmd)
        else:
            return m.groupdict()

class WIBOHostDevice(WIBORxDevice):
    """ Class to implement commands for WIBO Host """

    def __init__(self, *args, **kwargs):
        WIBORxDevice.__init__(self, *args, **kwargs)

    def ping(self, nodeid):
        ret = self._sendcommand('ping', hex(nodeid))
        if self.VERBOSE > 1:
                print ret
        if ret['code'] == 'OK':
            return eval(ret['data'])
        else:
            return None

    def finish(self, nodeid):
        return self._sendcommand('finish', hex(nodeid))

    def feedhex(self, nodeid, ln):
        return self._sendcommand('feedhex', hex(nodeid), ln)

    def reset(self):
        return self._sendcommand('reset')

    def exit(self, nodeid):
        return self._sendcommand('exit', hex(nodeid))

    def crc(self):
        return int(self._sendcommand('crc')['data'],16)

    def echo(self, dstr):
        return self._sendcommand('echo', dstr)

    def info(self, nodeid):
        return self._sendcommand('info', hex(nodeid))

    def xmpljbootl(self, nodeid):
        return self._sendcommand('xmpljbootl', hex(nodeid))

    def channel(self, channel):
        return self._sendcommand('channel', str(channel))



class WIBOHost(WIBOHostDevice):
    """ Class to represent a list of WIBO nodes """

    def __init__(self, *args, **kwargs):
        WIBOHostDevice.__init__(self, *args, **kwargs)
        self.devicelist = []

    def scan(self, scanrange=[]):
        ret = []
        if VERBOSE:
            print "scan nodes", scanrange,
        for c in CHANNELS:
            if VERBOSE:
                print "\nchan %d:" % c,
            self.channel(c)
            for a in scanrange:
                tmp = self.ping(a)
                if tmp != None:
                    tmp["chan"] = c
                    ret.append(tmp)
                    if VERBOSE:
                        print "0x%04x" % tmp['short_addr'],
                        if (len(ret) % 4) == 0:
                            print "\n        ",
        if VERBOSE:
            print "\nFound %d devices\n" % len(ret)
        return ret

    def flashhex(self, nodeid=0xFFFF, fname=None, delay=0.05):
        f=open(fname)
        self.reset()
        for i, ln in enumerate(f):
            self.feedhex(nodeid, ln.strip())
            time.sleep(delay)
            if VERBOSE == 1:
                print "line %-4d crc: 0x%04x\r" % (i, self.crc()),
                sys.stdout.flush()
            elif VERBOSE > 1:
                print i, ln.strip(), hex(self.crc())

        print "                      \r"\
              "file: %s, node: 0x%04x, records: %d, crc: 0x%04x" % \
              (fname, nodeid, i, self.crc())
        time.sleep(delay)
        self.finish(nodeid)
        f.close()
        return

    def checkcrc(self, nodeid):
        hostcrc = self.crc()
        p = self.ping(nodeid)
        if p:
            ret = (hostcrc == p['crc'])
            if ret:
                print "node 0x%04x crc OK" % nodeid
            else:
                print "node 0x%04x crc fail" % nodeid
                print "hostcrc: 0x%04x nodecrc: 0x%04x" % (hostcrc, p.get('crc'))
        else:
            print "no response from node 0x%04x" % nodeid
            ret = False
        return ret


def init_prompt():
    global HISTORY
    try:
        import readline, rlcompleter, atexit, sys, os
        sys.ps1 = "wibo>"
        readline.parse_and_bind("tab:complete")
        save_hist = lambda history : readline.write_history_file(history)
        atexit.register(readline.write_history_file, HISTORY)
        if os.path.exists(HISTORY):
            readline.read_history_file(HISTORY)
    except:
        print "No libreadline support"
        traceback.print_exc()

def param_evaluate_list(arg):
    lst = []
    for a in arg.split(","):
        a = a.split(":")
        if len(a)>1:
            lst += eval("range(%s,%s+1)" % (a[0],a[-1]))
        else:
            lst.append(eval(a[0]))
    lst.sort()
    return lst

def process_command_line():
    global PORT, BAUDRATE, ADDRESSES, WHOST, VERBOSE, CHANNELS, DELAY
    try:
        opts,args = getopt.getopt(sys.argv[1:],"C:P:A:U:u:hVSJvEd:")
    except getopt.GetoptError,e:
        print "="*80
        print "Error:", e
        print "="*80
        opts = (("-h",""),)

    ret = False
    for o,v in opts:
        if o == "-h":
            print __doc__
            ret = True
            break
        elif o == "-V":
            print "wibohost v.", VERSION
            ret = True
            break
        elif o == "-v":
            VERBOSE+=1
            WHOST.VERBOSE = VERBOSE
        elif o == "-d":
            DELAY = float(str(v))
        elif o == "-P":
            try:
                p,b = v.split(":")
                b = eval(b)
                PORT = p
                BAUDRATE = b
            except ValueError:
                PORT = v
            WHOST.close()
            WHOST.setPort(PORT)
            WHOST.setBaudrate(BAUDRATE)
            WHOST.open()
            for i in range(10):
                x = WHOST.echo("HalloWibo")
                if x["code"] == "OK":
                    break
                else:
                    time.sleep(.1)
            if i == 10:
                raise Exception("FATAL", "unable to connect wibo host %s" % x)
        elif o == "-A":
            ADDRESSES = param_evaluate_list(v)
        elif o == "-C":
            CHANNELS = param_evaluate_list(v)
        elif o == "-u":
            print "selective flashing nodes", ADDRESSES
            WHOST.channel(BL_CHANNEL)
            for n in ADDRESSES:
                print "flash node", n
                tmp = WHOST.ping(n)
                if tmp != None:
                    if tmp.get("appname") == "wibo":
                        WHOST.flashhex(n,v, DELAY)
                        print "CRC", WHOST.checkcrc(n)
                        print "EXIT", WHOST.exit(n)
                    else:
                        print "node %d is not responding" % n
                else:
                    print "node %d is not responding" % n

        elif o == "-U":
            NODES = WHOST.scan(range(min(ADDRESSES), max(ADDRESSES)+1))
            ADDRESSES = [n['short_addr'] for n in NODES]
            listeners = []
            WHOST.channel(BL_CHANNEL)
            for n in ADDRESSES:
                ret = WHOST.ping(n)
                if ret.get("appname") == "wibo":
                    listeners.append(n)
            if len(listeners) == 0:
                print "skip flashing, no listeners on channel %d" % BL_CHANNEL
            else:
                print "broadcast flashing nodes:", listeners
                WHOST.flashhex(0xffff,v, DELAY)
                for n in listeners:
                    print "CRC:",n, WHOST.checkcrc(n)
                    print "EXIT:",n, WHOST.exit(n)
        elif o == "-S":
            NODES = WHOST.scan(range(min(ADDRESSES), max(ADDRESSES)+1))
            ADDRESSES = [n['short_addr'] for n in NODES]
            if VERBOSE and len(NODES):
                pkeys = sorted(NODES[0].keys())
                print ", ".join(pkeys)
                for n in NODES:
                    print ", ".join([str(n[k]) for k in pkeys])
            print "SCAN:", ADDRESSES
        elif o == "-J":
            if VERBOSE:
                print "Jump to bootloader",
            for c in CHANNELS:
                if VERBOSE>1:
                    print "\nchannel: %d, node: " % c,
                WHOST.channel(c)
                for n in ADDRESSES:
                    if VERBOSE>1:
                        print n,
                    WHOST.xmpljbootl(n)
            if VERBOSE:
                print
            WHOST.channel(BL_CHANNEL)
            listeners = []
            for n in ADDRESSES:
                res = WHOST.ping(n)
                if res:
                    if res.get("appname") == "wibo":
                        listeners.append(n)

            print "Wibo listeners:", listeners



        elif o == "-E":
            WHOST.channel(BL_CHANNEL)
            for n in ADDRESSES:
                WHOST.exit(n)
                print "EXIT:",n, WHOST.exit(n)
    return ret

if __name__ == "__main__":
    WHOST = WIBOHost()
    do_exit = process_command_line()
    if do_exit:
        sys.exit(0)
    init_prompt()
