#!/usr/bin/python

# tz_serial_tcp.py
#       --copyright--                   Copyright 2011 (C) Tranzoa, Co. All rights reserved.    Warranty: You're free and on your own here. This code is not necessarily up-to-date or of public quality.
#       --url--                         http://www.tranzoa.net/tzpython/
#       --email--                       pycode is the name to send to. tranzoa.com is the place to send to.
#       --bodstamps--
#       October 7, 2011         bar     bring over various implementations of this to tzlib
#       October 9, 2011         bar     take out a debugging print
#                                       faster read sleep in main
#       November 23, 2011       bar     parity and stop bits
#       November 29, 2011       bar     pyflake cleanup
#       February 5, 2012        bar     --log_file
#                                       send keystrokes from main
#                                       more except catches
#       February 7, 2012        bar     define termios
#       March 2, 2012           bar     properly look for host:port
#                                       no, just do it better all around
#                                       some new baud rate cmd line params
#       March 8, 2012           bar     windows UnsupportedOperation exception handling (needs another way of seeing on-going errors !!!!)
#       March 25, 2012          bar     default the BAUD_RATE to 9600
#                                       do some futzing for weird ports that are half gone in linux
#       --eodstamps--
##      \file
#
#
#       Talk to a machine through serial port or through TCP.
#           Expose a readline() routine besides the usual byte oriented read/write routines.
#           There are read() and rx_ready() routines, too, but they have not been used and share the ibuf with readline().
#           Output is queued and put out in another thread so that it does not block.
#
#

import  os
import  Queue
import  re
import  socket
import  select
import  sys
import  threading
import  time


import  tzlib


try :
    import  pywintypes
except ImportError :
    pywintypes  = None


try :
    import  termios
except ImportError :
    class   a_termios(object) :
        def __init__(me) :
            me.error    = socket.error
        pass
    termios     = a_termios()


try :
    import  serial          # from pySerial
except ImportError :
    serial      = None



NOISY           = False     # do we print a lot of stuff?

BAUD_RATE       = 9600
PARITY          = serial.PARITY_NONE
STOP_BITS       = serial.STOPBITS_ONE

##              default tcp port number
TCP_PORT    =   23          # telnet port



class   a_cnct_exception(Exception) :
    """ Named exception class """
    pass



##              Parse a text line
crlf_re         = re.compile(r"([^\r\n]*)[\r\n]+(.*)",  re.DOTALL)


##                The types of connections we can make.


class a_cnct(threading.Thread) :
    """ Generic class to talk to something - sub-classed for the two real connection types, COM port and TCP. """


    class   a_debugger(object) :
        def __init__(me) :
            me.lock = threading.RLock()
            me.cnt  = 0
        def bump_cnt(me) :
            me.lock.acquire()
            me.cnt += 1
            me.lock.release()
        def down_cnt(me) :
            me.lock.acquire()
            me.cnt -= 1
            me.lock.release()
        #   a_debugger

    dbg             = a_debugger()


    def __init__(me, *args, **kwargs) :
        """ Constructor. """

        super(a_cnct, me).__init__(*args, **kwargs)

        me.dbg.bump_cnt()

        me.ibuf     = ""
        me.io       = None
        me.host     = None
        me.port     = None
        me.fo       = None
        # me.fo       = open("tz_serial_tcp.dat", "wb")

        me.icnt     = 0
        me.ocnt     = 0
        me.iwhen    = 0
        me.itry     = 0
        me.lock     = threading.RLock()

        me._stop    = False

        me.wq       = Queue.Queue()             # note: in the case of TCP, our thread isn't used

        me.setDaemon(True)


    def get_io(me) :
        me.lock.acquire()
        io  = me.io
        me.lock.release()
        return(io)


    def run(me) :
        """ Write output to the connection. """

        # print "tz_serial_tcp threadid %s:%s %d" % ( str(me.host), str(me.port), tzlib.get_tid() )

        while not me._stop  :
            what            = me.wq.get(True)
            if  what == None :  break

            c   = me.get_io()
            if  not c       :  break

            try :
                c.write(what)
                c.flush()
                me.ocnt    += len(what)
            except OSError  :
                me.blind_close()
                break
            except          :
                if  NOISY   :
                    tzlib.print_exception()
                    # print "tz_serial_tcp write error ignored and blind_close"
                me.blind_close()
                break
            if  not what    :
                time.sleep(0.02)
                pass
            pass
        pass


    def is_ok(me) :
        return(me.io and True)


    def rx(me, how_many = None) :
        """ Raw read the given number of bytes from the generic connection. """

        how_many    = how_many or 1

        c           = me.get_io()
        if  not c   :
            raise a_cnct_exception("Rx from no connection!")

        try         :
            if  me.fo :
                me.fo.write("ct")

            r       = c.read(how_many)                          # blocking call, perhaps
            t       = tzlib.elapsed_time()
            if  r and len(r)  :
                me.icnt    += len(r)
                me.iwhen    = t
            me.itry         = t

            if  me.fo :
                me.fo.write(r)

            pass

        except OSError :
            raise a_cnct_exception("OSError rx connection!")

        return(r or "")



    def read(me, how_many = 1) :
        """ Read the given number of bytes from the other end - with input buffering. """

        r   = ""
        if  how_many        > 0 :

            if  len(me.ibuf)    :
                r           = me.ibuf[0:how_many]
                me.ibuf     = me.ibuf[how_many:]

                how_many   -= len(r)

            if  how_many    > 0 :
                r          +=  me.rx(how_many)

            pass

        return(r)


    def readline(me) :
        """ Read None or a line of text from the other end. """

        while me.io     :
            if  not me.rx_i_ready() :
                break
            rb          = me.rx(None)           # blocking call. !!!! call rx with None and let rx go non-blocking if the param is none and let it get what there is to get
            if  not len(rb) :
                me.blind_close()
                raise a_cnct_exception("Read zero bytes: remote disconnect!")

            me.ibuf    += rb
        # print "@@@@", len(me.ibuf)

        g               = crlf_re.match(me.ibuf)
        if  g           :
            r           = g.group(1)
            me.ibuf     = g.group(2)
            if  r       :                       # don't return blank lines
                return(r)
            pass

        if  not me.io   :
            raise a_cnct_exception("Reading from no connection!")

        return(None)


    def rx_ready(me) :
        """ Is there anything ready to be read from the other end? """

        return(len(me.ibuf) + me.rx_i_ready())


    def write(me, what) :
        """ Write the given string to the other end. """

        if  not me.io   :
            raise a_cnct_exception("Write to no connection!")

        me.wq.put(what, True)

        return(len(what))



    def close(me) :
        """ Close the connection. """

        me._stop        = True
        me.lock.acquire()
        me.ibuf         = ""
        c               = me.io
        me.io           = None
        if  c           :
            if  NOISY   :
                ps      = "tz_serial_tcp %s:%s closing" % ( str(me.host), str(me.port) )
                print ps

            me.wq.put(None, True)

            try :
                try :
                    if  NOISY   :
                        print "CLOSING %s PORT **************************** %s %u %u %u %.1f %.1f *****************************"  % ( type(me).__name__, str(me.port), me.dbg.cnt, me.ocnt, me.icnt, me.iwhen, me.itry, )
                    if  isinstance(me, a_com) :
                        c.flushOutput()
                        c.flushInput()
                    c.close()
                    if  NOISY   :
                        print "CLOSED  %s PORT **************************** %s %u *****************************"                  % ( type(me).__name__, str(me.port), me.dbg.cnt )
                    me.dbg.down_cnt()
                    del(c)
                except ( socket.error, termios.error ) :
                    me.lock.release()
                    if  NOISY   :
                        print "tz_serial_tcp close socket.error"
                    raise a_cnct_exception("Close error!")
                except  :
                    me.lock.release()
                    e   = sys.exc_info()
                    if  pywintypes and (e[0] == pywintypes.error) :
                        if  NOISY   :
                            print "tz_serial_tcp close pywintypes.error"
                        raise a_cnct_exception("Close error!")
                    raise
                pass
            except NameError :                                              # in case an exception isn't defined
                try :
                    me.lock.release()
                except RuntimeError :
                    pass
                if  NOISY   :
                    print "tz_serial_tcp close NameError"
                raise a_cnct_exception("Close error!")
            pass

        me.lock.release()

        try :
            me.join(1.0)
        except RuntimeError :
            if  NOISY   :
                print "tz_serial_tcp close RuntimeError"
            pass

        if  me.fo   :
            me.fo.close()
            me.fo   = None

        if  NOISY   :
            ps          = "tz_serial_tcp %s:%s closed" % ( str(me.host), str(me.port) )
            print ps

        pass



    def blind_close(me) :
        """ Close the connection without fussing about whether anything goes wrong. """

        try :
            me.close()
        except a_cnct_exception :
            if  NOISY   :
                print "tz_serial_tcp blind_close exception"
            pass
        pass



    def __str__(me) :
        """ Return a string that represents us, like for use in the 'print' statement. """

        h   = me.host or ""
        if  h   : h   += ":"
        return("tz_serial_tcp." + type(me).__name__ + " %s%s" % ( h, str(me.port) ) )



    pass    # a_cnct




class   a_com(a_cnct) :
    """ Class to talk to a device through a COM/serial port. """

    def __init__(me, com_port, baud_rate = None, parity = None, stop_bits = None, *args, **kwargs) :
        """
            Raises SerialException if the port can't be opened.
        """

        super(a_com, me).__init__(*args, **kwargs)

        baud_rate   = baud_rate or BAUD_RATE
        if  baud_rate  >= 90000 :           # !!!! get a better table, handle strings like "115k"
            baud_rate   = 115200
        elif baud_rate >= 45000 :
            baud_rate   = 57600
        elif baud_rate >= 30000 :
            baud_rate   = 38400
        elif baud_rate == 115   :
            baud_rate   = 115200
        elif baud_rate == 114   :
            baud_rate   = 115200
        elif baud_rate == 192   :
            baud_rate   = 19200
        elif baud_rate == 384   :
            baud_rate   = 38400
        elif baud_rate == 38    :
            baud_rate   = 38400
        elif baud_rate == 19.2  :
            baud_rate   = 19200
        elif baud_rate == 19    :
            baud_rate   = 19200
        elif baud_rate == 96    :
            baud_rate   = 9600
        elif baud_rate == 48    :
            baud_rate   = 4800
        elif baud_rate == 24    :
            baud_rate   = 2400
        elif baud_rate == 12    :
            baud_rate   = 1200
        elif baud_rate == 3     :
            baud_rate   = 300

        me.io       = None

        com_port    = com_port or 1

        me.port     = com_port

        io          = None

        try :
            me.port = int(me.port)
            cport   = me.port - 1
        except ValueError :
            cport   = me.port

        if  not serial  :
            me.blind_close()
            raise a_cnct_exception(str(me) + " cannot be opened - no serial library!")

        try :
            if  NOISY   :
                print "OPENING %s PORT **************************** %s %u *****************************"        % ( type(me).__name__, str(me.port), me.dbg.cnt )
            io          = serial.Serial(port = cport, baudrate = baud_rate or BAUD_RATE, parity = parity or PARITY, stopbits = stop_bits or STOP_BITS, timeout = 0.001)
            if  NOISY   :
                print "OPENED  %s PORT **************************** %s %u *****************************"        % ( type(me).__name__, str(me.port), me.dbg.cnt )
            pass
        except serial.SerialException :
            if  NOISY   :
                print "OPENED  %s PORT **************************** %s %u ***************************** FAILED" % ( type(me).__name__, str(me.port), me.dbg.cnt )
            me.blind_close()
            raise a_cnct_exception(str(me) + " cannot be opened!")
        except          :
            if  NOISY   :
                print "OPENED  %s PORT **************************** %s %u ***************************** EXCEPT" % ( type(me).__name__, str(me.port), me.dbg.cnt )
                tzlib.print_exception()
            raise a_cnct_exception(str(me) + " exception!")

        if  hasattr(io, 'fd') :             # note: serialposix.py has the port opened as fd - it's possible to get out of serial.Serial() without the fd, the file, being opened and started OK.
            if  not io.fd   :
                if  NOISY   :
                    print "OPENED  %s PORT **************************** %s %u ***************************** ZERO"   % ( type(me).__name__, str(me.port), me.dbg.cnt )
                raise a_cnct_exception(str(me) + " no fd!")

            try     :
                io.fd + 1
            except  :
                if  NOISY   :
                    print "OPENED  %s PORT **************************** %s %u ***************************** fd"     % ( type(me).__name__, str(me.port), me.dbg.cnt )
                raise a_cnct_exception(str(me) + " non-numeric fd!")
                if  NOISY   :
                    tzlib.print_exception()
                raise

            # print "tz_serial_tcp: Port", cport, io.fd, io.xonxoff, io.timeout, io.rtscts, io.dsrdtr

        me.lock.acquire()
        me.io   = io
        me.lock.release()


    def is_ok(me) :
        if  not super(a_com, me).is_ok() :
            return(False)

        c           = me.get_io()
        if  c and hasattr(c, 'fileno') :
            try     :
                try     :
                    c.fileno()
                except serial.portNotOpenError  :           # note: Windows code has SerialException - is that better or a version thing?
                    c   = None
                except serial.SerialException   :
                    c   = None
                except  :
                    e   = sys.exc_info()
                    if  str(e).find('UnsupportedOperation') < 0 :
                        raise
                    pass                                    # windows doesn't have fileno, really. It raises io.UnsupportedOperation.
                pass
            except  ( NameError, AttributeError )   :       # in case an exception isn't defined
                c       = None
            pass

        return(c and True)


    def rx_i_ready(me) :
        """ Raw, is there anything ready to be read from the other end? """

        me.itry = tzlib.elapsed_time()
        c       = me.get_io()
        if  not c :
            raise a_cnct_exception("i_ready no connection!")

        try     :
            return(c.inWaiting())
        except  ( serial.SerialException, serial.serialutil.SerialException, OSError, IOError, AttributeError ) :
            pass
        raise   a_cnct_exception("rx_i_ready error!")



    def rx(me, how_many = None) :
        """ Raw read the given number of bytes from the other generic connection. """

        if  how_many   == None :
            how_many    = me.rx_i_ready()

        try :
            r   = super(a_com, me).rx(how_many)
        except  ( serial.SerialException, serial.serialutil.SerialException, OSError, IOError, AttributeError ) :
            raise a_cnct_exception("read error!")
        return(r)



    pass    # a_com



class   a_tcp(a_cnct) :
    """ Class to talk to a other end through a TCP connection. """


    def __init__(me, host, port, *args, **kwargs) :
        """ Constructor. """

        super(a_tcp, me).__init__(*args, **kwargs)

        host    = host or "localhost"
        port    = port or TCP_PORT

        me.host = host
        me.port = port

        io      = None
        try :
            for res in socket.getaddrinfo(me.host, me.port, socket.AF_UNSPEC, socket.SOCK_STREAM) :
                ( af, socktype, proto, canonname, sa ) = res

                try :
                    io  = socket.socket(af, socktype, proto)
                    try :
                        io.connect(sa)
                        break
                    except socket.error :
                        me.blind_close()
                    pass
                except socket.error :
                    io  = None
                pass
            pass
        except  socket.gaierror :
            if  NOISY :
                tzlib.print_exception()
                print "host:port", me.host, me.port
            io  = None

        if  io  is None :
            me.blind_close()
            raise a_cnct_exception(str(me) + " cannot be opened!")

        me.lock.acquire()
        me.io   = io
        me.lock.release()


    def start(me) :
        pass                                    # we don't use the queue and thread for writing



    def rx_i_ready(me) :
        """ Raw, is there anything ready to be read from the other end? """

        c   = me.get_io()
        if  not c :
            raise a_cnct_exception(str(me) + " i_ready closed connection.")
            return(0)

        rs      = []
        try :
            rs  = select.select([c], [], [c], 0)
        except select.error :
            me.blind_close()
            rs  = []
            raise a_cnct_exception(str(me) + " i_ready broken connection.")

        if  len(rs[2]) > 0 :
            me.blind_close()
            raise a_cnct_exception(str(me) + " i_ready broken connection.")

        return(((len(rs[0]) > 0) and 1) or 0)



    def rx(me, how_many = 1) :
        """ Raw read the given number of bytes from the TCP connection. """

        c       = me.get_io()
        if  c   :
            if  how_many   == None :
                c.setblocking(False)
                how_many    = 4096
            else :
                how_many    = how_many or 1

            try :
                r           = c.recv(how_many)
                me.icnt    += len(r)

                return(r)

            except socket.error :
                raise a_cnct_exception(str(me) + " read broken connection.")
            pass

        me.close()

        return("")



    def write(me, what) :
        """ Write the given string to the TCP connection. """

        c   = me.io
        if  not c :
            raise a_cnct_exception("Write to no connection!")

        try :
            c.sendall(what)
            me.ocnt    += len(what)
        except socket.error :
            me.blind_close()

            raise a_cnct_exception(str(me) + " write broken connection.")

            return(-1)

        return(len(what))



    def close(me) :
        """ Close the connection. """

        me._stop    = True
        me.ibuf     = ""

        me.lock.acquire()

        c           = me.io
        if  c       :
            try     :
                c.setblocking(False)
            except socket.error :
                pass

            try     :
                if  NOISY   :
                    print "tz_serial_tcp shutting down"
                c.shutdown(socket.SHUT_RDWR)
                if  NOISY   :
                    print "tz_serial_tcp shut down"
                pass
            except socket.error :
                if  NOISY   :
                    print "tz_serial_tcp shutdown socket.error"
                pass
            pass

        me.lock.release()

        super(a_tcp, me).close()



    pass    # a_tcp





def cmd_line_baud_rate(baud_rate) :
    """ Convert a command line argument baud rate to an integer rate. """

    baud_rate       = float(baud_rate)
    if (baud_rate  == 114) :
        baud_rate   = 115200
    if (baud_rate  == 56) or (baud_rate  == 57) :
        baud_rate   = 57600
    if (baud_rate  == 38) :
        baud_rate   = 38400
    if (baud_rate  == 19.2) or (baud_rate == 19) or (baud_rate == 192) :
        baud_rate   = 19200
    if (baud_rate  == 14.4) or (baud_rate == 14) or (baud_rate == 144) :
        baud_rate   = 14400
    if (baud_rate  == 96) :
        baud_rate   = 9600
    baud_rate       = int(baud_rate)

    return(baud_rate)




##      Command line help string.
help_str    = """
%s      (COM_port__or__host(:port))

Talks to a machine through a (USB) COM port or TCP.

Default host_port:          localhost:%u

Options:

    --baud_rate     rate        Set the baud rate of a serial port. (default:%u)
    --log_file      file_name   Append all input to the given log file.


"""


#
#
#
if  __name__ == '__main__' :
    """
        Just be a simple terminal emulator.
        Single keys are sent to the connected machine.
        Text lines from the connected machine are printed out.
    """

    import  TZCommandLineAtFile
    import  TZKeyReady


    program_name    = sys.argv.pop(0)
    TZCommandLineAtFile.expand_at_sign_command_line_files(sys.argv)


    baud_rate           = None
    ofile_name          = None

    if  tzlib.array_find(sys.argv, [ "--help", "-h", "-?", "/?", ] ) >= 0 :
        print help_str % ( os.path.basename(program_name), TCP_PORT, BAUD_RATE, )
        sys.exit(254)


    while True :
        oi  = tzlib.array_find(sys.argv, [ "--baud_rate", "--baudrate", "--baud-rate", "--baud", "--rate", "--br", ])
        if  oi < 0 :    break
        del sys.argv[oi]
        baud_rate   = cmd_line_baud_rate(sys.argv.pop(oi))

    while True :
        oi  = tzlib.array_find(sys.argv, [ "--log_file", "--log-file", "--logfile", "--output_file", "--output-file", "--outputfile", "--lf", "--of", "-l", "-o", ])
        if  oi < 0 :    break
        del sys.argv[oi]
        ofile_name      = sys.argv.pop(oi)



    cnct_to     = "localhost:%s" % ( TCP_PORT )



    if  len(sys.argv) > 0 :
        cnct_to       = sys.argv.pop(0)



    com_port        = None
    host            = None
    port            = TCP_PORT

    if  cnct_to.isdigit() or (cnct_to.find('/dev/tty') >= 0) or re.match(r"^\s*com\s*\d+\:?\s*$", cnct_to.lower()) :
        me          = a_com(cnct_to, baud_rate = baud_rate)
    else            :
        g           = re.match(r"([^\:\/]+)(?:\:(\d+))?$", cnct_to)     # look for host:port
        if  not g   :
            me      = a_com(cnct_to, baud_rate = baud_rate)
            # raise "Cannot understand connection to " + cnct_to
        else :
            host        = g.group(1)
            if  g.lastindex > 1 :
                port    = int(g.group(2))
            me          = a_tcp(host, port)
        pass

    me.start()

    fo          = None
    if  ofile_name :
        fo      = open(ofile_name, "ab")

    try         :
        while True  :
            try     :
                li  = me.readline()
            except a_cnct_exception :
                e   = sys.exc_info()
                print >> sys.stderr, str(e[0]), str(e[1])
                break

            if  not li :
                time.sleep(0.02)
            else    :
                if  fo :
                    fo.write(li)
                    if  not li.endswith('\n') :
                        fo.write('\n')
                    pass

                li  = li.strip()

                li  = re.sub(r"[^ -~]", "", li)

                print ">>>", li[0:160]                             # here is an input line from the machine we are talking to

            while True :
                k   = TZKeyReady.tz_key_ready()
                if  k == None :
                    break
                print k

                try :
                    if  fo :
                        fo.write("\nKEY:%s %u %02x\n" % ( " " + k if ' ' <= k <= '~' else "", ord(k), ord(k) ) )
                    me.write(k)
                except a_cnct_exception :
                    print >> sys.stderr, "Disconnected!"
                pass
            pass
        pass
    except KeyboardInterrupt :
        print

    if  fo :
        fo.close()

    try :
        me.close()
    except a_cnct_exception :
        e   = sys.exc_info()
        print >> sys.stderr
        print >> sys.stderr
        print >> sys.stderr, "Close error", str(e[0]), str(e[1])

    pass



#
#
# eof

