import sys,os,select,time,random,re,binascii
import serial

class Altec:
    def __init__(self,port,baudrate=9600,verbose=0,instrumentaddress="01"):
        self.instrumentaddress=instrumentaddress
        self.prompt=">"
        self.timeout=5.0
        self.baudrate=baudrate
        self.verbose=verbose
        self.device=port

        self.connect()

    def connect(self):
        self.connectionfailed=0
        # startbits is always 1?
        try:
            self.port=serial.Serial(self.device,baudrate=self.baudrate,parity=serial.PARITY_EVEN,stopbits=1,bytesize=7,timeout=3)
        except:
            self.connectionfailed=1        

    def initialize(self):
        pass

    def write(self,cmd):
        if self.connectionfailed: return 
        try:
            self.port.flushInput()
            self.port.write(cmd)
            self.port.flush()
        except:
            self.port.close()
            self.connectionfailed=1

    def send(self,cmd,wait=3.0,verbose=1):
        self.write(cmd.encode("utf-8")+b"\r")
        response=self.waitresponse(wait)
        if self.verbose: print("cmd",cmd,"got response",response)        
        return response

    def waitresponse(self,wait=3.0):

        STX=bytes([2])
        ETX=bytes([3])
        EOT=bytes([4])
        ENQ=bytes([5])
        ACK=bytes([6])
        NAK=bytes([0x15])
        
        t0=time.time()
        endprompt=0
        data=b""
        err=-1
        while 1:
            if self.connectionfailed: return -100,[]
            t1=time.time()
            avail_read, avail_write, avail_error = select.select([self.port],[],[], 0.1)
            for d in avail_read:
                try:
                    data+=d.read()
                except:
                    self.connectionfailed=1                    
            #print("data",data,data.endswith(ACK),data.endswith(NAK),(len(data)>1 and data[-2]==ETX[0]),data[-2] if len(data)>1 else 0,ETX)
            if data.endswith(ACK) or data.endswith(NAK) or (len(data)>1 and data[-2]==ETX[0]):
                err=0
                break
            if time.time()-t0>wait:
                # timeout
                #print("timing out",repr(data))
                break
        data=data.decode("utf-8","ignore")
        return err,data


class PC410:
    def __init__(self,port,baudrate=9600,verbose=1,instrumentaddress="01"):
        self.altec=Altec(port,baudrate=baudrate,verbose=verbose,instrumentaddress=instrumentaddress)

    def initialize(self):
        self.altec.initialize()

    def bcc(self,block):
        xcc=0
        for b in block:
            xcc^=ord(b)
        return xcc
        
    def handleresponse(self,e1,r1,start=None,cmd=""):
        data=None
        index=0

        # [STX](C1)(C2)<DATA>[EXT](BCC) 
        STX=chr(2)
        ETX=chr(3)
        EOT=chr(4)
        ENQ=chr(5)
        ACK=chr(6)
        NAK=chr(0x15)
        if e1==0 and r1:
            while 1:
                if index>=len(r1):
                    # should we have our own?
                    r="NO RESPONSE"
                    e1=-5
                else:
                    r=r1[index:]
                    #print(index,repr(r),repr(start))
                    if r.startswith(ACK):
                        # no response. we would already have err -3 from send call so won't get here
                        e1=0
                    elif r.startswith(NAK):
                        e1=-1
                    elif start!=None and r.startswith(start):
                        # valid response - data to return
                        data=r1[index][len(start):]

                        if r[0]==STX and len(r)>1 and r[-2]==ETX:                            
                            bcc=ord(r[-1])
                            block=[r[x] for x in range(1,len(r)-1)]
                            xcc=self.bcc(block)
                            if xcc!=bcc:
                                print("bcc wrong",xcc,bcc)
                                e1=-6
                            else:
                                #print("bcc ok",xcc,bcc)
                                pass
                            data=r[len(start):len(r)-2]
                            #print("returning data",repr(data))
                        else:
                            # no etx second last byte?!?
                            e1=-5
                    else:
                        # broken response
                        e1=-4
                        pass
                break
        return e1,data


    def send(self,msg,wait):
        return self.altec.send(msg,wait)
    
    def read(self,parameter,verbose=1,wait=3.0):
        """
        02 stx start of text
        03 etx end of text
        04 eot end of transmission
        05 enq enquiry
        06 ack positive ack
        15 nak negative ack
        20 space
        2d -
        2e .
        3e >
        30-39 0-9

        read 
        [EOT](ADR_H)(ADR_H)(ADR_L)(ADR_L)(C1)(C2)[ENQ]

        addr is instrument address, ours is 01
        baud is set to 9600

        cmd 0011PV got response (-1, ['\x02PV   9.\x032'])
        read response 0011PV -1 ['\x02PV   9.\x032']

        [STX](C1)(C2)<DATA>[EXT](BCC)

        BCC
        This is a block checksum that is generated for data validation. It is computed by XORing(exclusive or)
        all the characters after and excluding the STX, and including the ETX. Note that it may take the value
        of 'EOT' and care must be take when writing a protocol driver to ensure that this is not seen as an 'End of
        Transmission' sequence.

        write
        [EOT](ADR_H)(ADR_H)(ADR_L)(ADR_L)[STX](C1) (C2)<DATA>[ETX](BCC)

        The value of the parameter in a given display format. e.g. 99.9,1.2, -999, >1234 etc.
        BCC
        This is a block checksum that is generated for data validation. It is computed by XORing(exclusive or)
        all the characters after and excluding the STX, and including the ETX.

        response is ACK or NAK

        program parameter list
        Lc  loop counter
        r1  ramp rate 1
        L1  target setpoint 1
        dl  dwell time
        r2  ramp rate 2
        L2  target setpoint 2
        d2  dwell time 2
        PL1 ramp 1 and dwell 1 output power limit
        PL2 ramp 2 and dwell 2 output power limit


        PV               last measure value read only
        OP               output power read only
        SP               setpoint read only
        SL               local set point 1
        ...


        P.End can be Off or SP constant temp or a program number to continue with that program
        there are 10 programs of 16 segments that each can repeat 1-200 times or continuously?

        
        """
        STX=chr(2)
        ETX=chr(3)
        EOT=chr(4)
        ENQ=chr(5)
        ACK=chr(6)
        NAK=chr(0x15)

        
        addrhi=self.altec.instrumentaddress[0]
        addrlo=self.altec.instrumentaddress[1]
        msg=[EOT,addrhi,addrhi,addrlo,addrlo,parameter[0],parameter[1],ENQ]
        msg="".join(msg)
        #print(repr(msg))
        e1,r1=self.send(msg,wait=wait)
        
        e1,data=self.handleresponse(e1,r1,start=STX+parameter[0]+parameter[1])
        #if verbose: print("read response",msg,e1,repr(r1))
        return e1,data

    def write(self,parameter,data,verbose=1,wait=3.0,hex=0):
        # data must be hex string ready to write
        STX=chr(2)
        ETX=chr(3)
        EOT=chr(4)
        ENQ=chr(5)
        ACK=chr(6)
        NAK=chr(0x15)

        
        addrhi=self.altec.instrumentaddress[0]
        addrlo=self.altec.instrumentaddress[1]

        
        # data is value in display format e.g. 99.9,1.2, -999,>1234
        if type(data)!=str: data=str(data)
        sdata=[c for c in data]
        msg=[EOT,addrhi,addrhi,addrlo,addrlo,STX,parameter[0],parameter[1]]+sdata+[ETX]
        # bcc is block checksum xor all characters after and excluding stx and including etx
        bcc=self.bcc(msg[6:])
        msg+=[chr(bcc)]

        msg="".join(msg)

        retries=3
        for i in range(0,retries):
            # just in case wait
            time.sleep(0.1)
            wait1=0.2
            e1,r1=self.send(msg,wait=wait1)
            e2,data=self.handleresponse(e1,r1)
            if verbose: print("write response",repr(msg),e1,e2,r1,repr(data))
            if e2==0: break
        return e2,data

def main(argv,stdout,environ):
    progname = argv[0]
    args=argv[1:]

    port="/dev/ttyUSB0"
    readparam=None
    writeaddr=None
    writevalue=None
    baudrate=9600
    i=0
    while i<len(args):
        if args[i]=="-p":
            i+=1
            port=args[i]
        elif args[i]=="-b":
            i+=1
            baudrate=int(args[i])
        elif args[i]=="--read":
            i+=1
            readparam=args[i].strip()
        elif args[i]=="--write":
            i+=1
            writeaddr=args[i]
            i+=1
            try:
                writevalue=int(args[i],16)
            except:
                try:
                    writevalue=float(args[i])
                except:
                    writevalue=args[i]
                
        i+=1

    pc410=PC410(port,baudrate,verbose=0)
    
    if readparam!=None:
        t0=time.time()
        value=pc410.read(readparam)
        dt=time.time()-t0
        print("read",readparam,"returns",value,"took",dt)
        params=["PV","OP","SP","SL","HA","LA","DA","XP","TI","TD","HB","LB","CH","CC","RG","HS","LS","BP","HO","SR","HB","Lc","SW","XS","OS"]

        for j in range(1,9):
            params+=["r%d"%j,"l%d"%j,"t%d"%j]
            # rn is slope/rate
            # dn == tn is how long to dwell
            # ln is what level ie what temperature
        
        for readparam in params:
            e1,value=pc410.read(readparam)
            print("read",readparam,"returns",e1,value,"took",dt)
            if readparam=="SW":
                value1=int(value[1:],16)
                bits=["Data format",
                      "Input fault",
                      "Barring key operation",
                      "---",
                      "---",
                      "Modifying parameters by keys",
                      "Deviation alarm status",
                      "Deviation alarm condition",
                      "Lower limit alarm status",
                      "Lower limit alarm condition",
                      "Upper limit alarm status",
                      "Upper limit alarm condition",
                      "Alarm output",
                      "---",
                      "---",
                      "Auto/manual"]
                for i in range(0,len(bits)):
                    print("   %d %s"%((value1>>i)&1,bits[i]))
            if readparam=="XS":
                value1=int(value[1:],16)
                print("   %d %s"%(value1,"Turn on self-tuning"))
            if readparam=="OS":
                value1=int(value[1:],16)
                print("   %d %s"%(value1,"0=stop program, 2=run program, 3=hold program"))

        # not only display format we can write with or without decimal point, with or without leading space
        #e1,r1=pc410.write("l1","143")
        e1,r1=pc410.write("l1",147)
        print("write returned",e1,r1)

        readparam="l1"
        e1,value=pc410.read(readparam)
        print("read",readparam,"returns",e1,value)
                      
        if 0:
            for i in range(0,26):
                for j in range(0,26):
                    c1=chr(65+i)
                    c2=chr(65+j)
                    param=c1+c2
                    print(param)
                    e1,r1=pc410.read(param,wait=3.0)
                    if e1!=-1:
                        print("read",param,"returns",repr(value))

    if writeaddr:
        ok=pc410.write(writeaddr,writevalue)


    print("done")

    
if __name__=='__main__':
    main(sys.argv, sys.stdout, os.environ)

    
