#!/usr/bin/python
'''
 This is a pseudo-library implementation
 
 Example:
   ./pseudo-library.py -t b168ccc8c8734fad98323247afbc1113 --dump

 Author:   Volodymyr Shymanskyy
 License:  The MIT license
'''
import select, socket, struct
import os, sys, time, getopt
from threading import Thread

# Configuration options

# Parse command line options
try:
    opts, args = getopt.getopt(sys.argv[1:],
        "hs:p:t:",
        ["help", "server=", "port=", "token=", "sndbuf=", "rcvbuf=", "nodelay=", "dump"])
except getopt.GetoptError:
    print >>sys.stderr, __doc__
    sys.exit(2)

# Default options
SERVER = "blynk-cloud.com"
PORT = 8442
NODELAY = 1     # TCP_NODELAY
SNDBUF = 0      # No SNDBUF override
RCVBUF = 0      # No RCVBUF override
TOKEN = "YourAuthToken"
DUMP = 0

for o, v in opts:
    if o in ("-h", "--help"):
        print __doc__
        sys.exit()
    elif o in ("-s", "--server"):
        SERVER = v
    elif o in ("-p", "--port"):
        PORT = int(v)
    elif o in ("-t", "--token"):
        TOKEN = v
    elif o in ("--sndbuf",):
        SNDBUF = int(v)
    elif o in ("--rcvbuf",):
        RCVBUF = int(v)
    elif o in ("--nodelay",):
        NODELAY = int(v)
    elif o in ("--dump",):
        DUMP = 1


# Blynk protocol helpers

hdr = struct.Struct("!BHH")

class MsgType:
    RSP    = 0
    LOGIN  = 2
    PING   = 6
    BRIDGE = 15
    HW     = 20

class MsgStatus:
    OK     = 200

def hw(*args):
    # Convert params to string and join using \0
    data = "\0".join(map(str, args))
    dump("< " + " ".join(map(str, args)))
    # Prepend HW command header
    return hdr.pack(MsgType.HW, genMsgId(), len(data)) + data

def handle_hw(data):
    params = data.split("\0")
    cmd = params.pop(0)
    if cmd == 'info':
        pass

    ### DIRECT pin operations
    elif cmd == 'pm':
        pairs = zip(params[0::2], params[1::2])
        for (pin, mode) in pairs:
            pin = int(pin)
            if mode == 'in':
                log("Pin %d mode INPUT" % pin)
            elif mode == 'out':
                log("Pin %d mode OUTPUT" % pin)
            elif mode == 'pu':
                log("Pin %d mode INPUT_PULLUP" % pin)
            elif mode == 'pd':
                log("Pin %d mode INPUT_PULLDOWN" % pin)
            else:
                log("Unknown pin %d mode: %s" % (pin, mode))

    elif cmd == 'dw':
        pin = int(params.pop(0))
        val = params.pop(0)
        log("Digital write pin %d, value %s" % (pin, val))
        
    elif cmd == 'aw':
        pin = int(params.pop(0))
        val = params.pop(0)
        log("Analog write pin %d, value %s" % (pin, val))
        
    elif cmd == 'dr':           # This should read digital pin
        pin = int(params.pop(0))
        log("Digital read pin %d" % pin)
        conn.sendall(hw("dw", pin, 1)) # Send value

    elif cmd == 'ar':           # This should do ADC read
        pin = int(params.pop(0))
        log("Analog read pin %d" % pin)
        conn.sendall(hw("aw", pin, 123)) # Send value

    ### VIRTUAL pin operations
    elif cmd == 'vw':           # This should call user handler
        pin = int(params.pop(0))
        val = params.pop(0)
        log("Virtual write pin %d, value %s" % (pin, val))
        
    elif cmd == 'vr':           # This should call user handler
        pin = int(params.pop(0))
        log("Virtual read pin %d" % pin)
        conn.sendall(hw("vw", pin, "hello")) # Send value
        
    else:
        log("Unknown HW cmd: %s" % cmd)

static_msg_id = 1
def genMsgId():
    global static_msg_id
    static_msg_id += 1
    return static_msg_id

# Other utilities

start_time = time.time()
def log(msg):
    print "[{:7.3f}] {:}".format(float(time.time() - start_time), msg)

def dump(msg):
    if DUMP:
        log(msg)

def receive(sock, length):
    d = []
    l = 0
    while l < length:
        r = ''
        try:
            r = sock.recv(length-l)
        except socket.timeout:
            continue
        if not r:
            return ''
        d.append(r)
        l += len(r)
    return ''.join(d)

# Threads

def readthread(conn):
    while (True):
        data = receive(conn, hdr.size)
        if not data:
            break
        msg_type, msg_id, msg_len = hdr.unpack(data)
        dump("Got {0}, {1}, {2}".format(msg_type, msg_id, msg_len))
        if msg_type == MsgType.RSP:
            pass
        elif msg_type == MsgType.PING:
            log("Got ping")
            # Send Pong
            conn.sendall(hdr.pack(MsgType.RSP, msg_id, MsgStatus.OK))
        elif msg_type == MsgType.HW or msg_type == MsgType.BRIDGE:
            data = receive(conn, msg_len)
            # Print HW message
            dump("> " + " ".join(data.split("\0")))
            handle_hw(data)
        else:
            log("Unknown msg type")
            break


def writethread(conn):
    while (True):
        time.sleep(10)
        log("Sending heartbeat...")
        conn.sendall(hdr.pack(MsgType.PING, genMsgId(), 0))

# Main code
log('Connecting to %s:%d' % (SERVER, PORT))
try:
    conn = socket.create_connection((SERVER, PORT), 3)
except:
    log("Can't connect")
    sys.exit(1)

if NODELAY != 0:
    conn.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
if SNDBUF != 0:
    sndbuf = conn.getsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF)
    log('Default SNDBUF %s changed to %s' % (sndbuf, SNDBUF))
    conn.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, SNDBUF)
if RCVBUF != 0:
    rcvbuf = conn.getsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF)
    log('Default RCVBUF %s changed to %s' % (rcvbuf, RCVBUF))
    conn.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, RCVBUF)
    
# Authenticate
conn.sendall(hdr.pack(MsgType.LOGIN, genMsgId(), len(TOKEN)))
conn.sendall(TOKEN)
data = receive(conn, hdr.size)
if not data:
    log("Auth timeout")
    sys.exit(1)

msg_type, msg_id, status = hdr.unpack(data)
dump("Got {0}, {1}, {2}".format(msg_type, msg_id, status))

if status != MsgStatus.OK:
    log("Auth failed: %d" % status)
    sys.exit(1)

wt = Thread(target=readthread,  args=(conn,))
rt = Thread(target=writethread, args=(conn,))

wt.start()
rt.start()

wt.join()
rt.join()

conn.close()
