#!/usr/bin/python
# -*- coding: utf-8 -*-

# $Header: https://svn.mpia.de/gulli/TwiceAsNice/branch/unstable/TwiceAsNice/basda-mocca/scripts/dumpMoConParams.py 14057 2016-11-10 14:53:04Z mathar $

## @file dumpMoConParams.py
# Translate a MPIA module data sheet to a TAN configuration file.


from Nice import *
from Basda import *
from moConParamsBase import *
import os.path

import pexpect
import telnetlib


class Dump(Base):
    def __init__(self, _cfg, _type, _vendor, _product, _encoderType, _file, _path, _verbose):        
        self.config     = _cfg
        self.type       = _type
        self.vendor     = _vendor
        self.product    = _product
        self.encoderType = _encoderType
        if (_file != None):
            self.filename = _file
        else: 
            self.filename   = _cfg.node("CFG.DATASHEET.CONFIG").String         
        self.path       = _path       
        self.verbose    = _verbose
        self.configFile = ""
        
        self.telnet = None
        
    ## Query parameters from the electronics: GetBasicSettings .. GetEncode Settings.
    # This executes commands 150 to 154 in the programmer's guide.
    def now(self):
        I_LOG("Dump config parameter.")
        # getBasicSettings
        self.telnet.msg(self.telnet.write(str(self.card) + ' 150 ' + str(self.module) + '\r'))
        self.basicSettings = self.telnet.read_until(str(self.card) + ' 150 ' + str(self.module) + ' 1', self.connectionTimeout.toSeconds())
        self.basicSettings = self.basicSettings[self.basicSettings.find(str(self.card)):] # will remove a leading \r\n
        self.telnet.msg(self.basicSettings)
        # getMotorSettings
        self.telnet.msg(self.telnet.write(str(self.card) + ' 151 ' + str(self.module) + '\r'))
        self.motorSettings = self.telnet.read_until(str(self.card) + ' 151 ' + str(self.module) + ' 1', self.connectionTimeout.toSeconds())
        self.motorSettings = self.motorSettings[self.motorSettings.find(str(self.card)):] # will remove a leading \r\n
        self.telnet.msg(self.motorSettings)
        # getStepperSettings
        self.telnet.msg(self.telnet.write(str(self.card) + ' 152 ' + str(self.module) + '\r'))
        l = [str(self.card) + ' 152 ' + str(self.module) + ' 1', str(self.card) + ' 152 ' + str(self.module) + ' -10 1']
        ret = self.telnet.expect(l, self.connectionTimeout.toSeconds())
        if (ret[0] != 0):
            D_LOG('Module is not stepper. Error ignored.')
            self.stepperSettings = None
        else:
            self.stepperSettings = ret[2][ret[2].find(str(self.card)):] # will remove a leading \r\n
        self.telnet.msg('stepperSettings: ' + str(self.stepperSettings))
        # getServoLoopSettings
        self.telnet.msg(self.telnet.write(str(self.card) + ' 153 ' + str(self.module) + '\r'))
        l = [str(self.card) + ' 153 ' + str(self.module) + ' 1', str(self.card) + ' 153 ' + str(self.module) + ' -10 2']
        ret = self.telnet.expect(l, self.connectionTimeout.toSeconds())
        if (ret[0] != 0):
            D_LOG('Module is not servo. Error ignored')
            self.servoSettings = None    
        else:
            self.servoSettings = ret[2][ret[2].find(str(self.card)):] # will remove a leading \r\n
        self.telnet.msg('servoSettings: ' + str(self.servoSettings))
        # getEncoderSettings
        self.telnet.msg(self.telnet.write(str(self.card) + ' 154 ' + str(self.module) + '\r'))
        self.encoderSettings = self.telnet.read_until(str(self.card) + ' 154 ' + str(self.module) + ' 1', self.connectionTimeout.toSeconds())
        self.encoderSettings = self.encoderSettings[self.encoderSettings.find(str(self.card)):] # will remove a leading \r\n
        self.telnet.msg(self.encoderSettings)
        
    def addTo(self, _line=""):
        self.configFile = self.configFile + _line + "\n"
        
    ## Write a TAN configuration file to the specified file.
    def writeConfigFile(self):
        I_LOG("write config file: " + self.path + "/" + self.filename)
        self.addTo('###########################################################################################')
        self.addTo('## Config file generated from python script: \"' + self.filename + '\"')
        self.addTo('##')
        self.addTo('## Created')
        self.addTo('##      by: ' + os.path.basename(__file__))
        self.addTo('##')
        self.addTo('## WARNING! All changes made in this file will be lost executing ' + os.path.basename(__file__) + ' script!')
        self.addTo('###########################################################################################')
        self.addTo('')
        self.addTo('# predefined properties with default values')
        self.addTo('# needed by MoCon service')
        self.addTo('TYPE:String    = "' + self.type + '"')
        self.addTo('VENDOR:String  = "' + self.vendor + '"')
        self.addTo('PRODUCT:String = "' + self.product + '"')
        self.addTo('')
        self.addTo('# enable this flag to flash the MoCon with these settings ')
        self.addTo ('UPLOAD_SETTINGS:Bool = 0')
        self.addTo('')
        self.addTo('# external profile settings')
        self.addTo('PROFILE.SEGMENTS_PER_BLOCK:U32 = 5')
        self.addTo('TIME_PER_CYCLE:Double = 614.411  +UNIT="microsecond"')
        self.addTo('')
        self.addTo('# trace settings: SAMPLE_MODE:  0=one time; 1=rolling buffer')
        self.addTo('#                 SAMPLE_RATE:  measure every 1..65000 cycle.')
        self.addTo('TRACE.SAMPLE_MODE:Int = 0')
        self.addTo('TRACE.SAMPLE_RATE:Int = 1  +MIN:Int=1  +MAX:Int=65000')
        self.addTo('')
        self.addTo('')
        self.addTo('# amplifier settings can not be downloaded. Properties are defined with default values!')
        self.addTo('# amplifier settings: BITMASK:       bit mask for microsteps: 0x0..0xFF')
        self.addTo('#                     SENSE_CONTROL: sense control signal: 0x0..0x3F')
        self.addTo('#                                    bit 0: PWMa/Pulse     (0=not inverse, 1=inverse)')
        self.addTo('#                                    bit 1: PWMb           (0=not inverse, 1=inverse)')
        self.addTo('#                                    bit 2: PWMc/AtRest    (0=not inverse, 1=inverse)')
        self.addTo('#                                    bit 3: Sign/Dir       (0=not inverse, 1=inverse)')
        self.addTo('#                                    bit 4: power off      (0=not inverse, 1=inverse)')
        self.addTo('#                                    bit 5: auto power off (0=not inverse, 1=inverse)')
        self.addTo('AMPLIFIER.BITMASK:U16             = 0  +MIN:U16=0  +MAX:U16=255')
        self.addTo('AMPLIFIER.MICROSTEPS.BITVALUE:U16 = 0  +MIN:U16=0  +MAX:U16=255')
        self.addTo('AMPLIFIER.MICROSTEPS.POSITION:U16 = 1  +MIN:U16=1  +MAX:U16=16')
        self.addTo('AMPLIFIER.MICROSTEPS.VALUE:U16    = 1  +MIN:U16=0  +MAX:U16=1024')
        self.addTo('AMPLIFIER.SENSE_CONTROL:BitSet    = 0x0:6 +LABEL:SeqString=["Bit 0: PWMa / Pulse","Bit 1: PWMb","Bit 2: PWMc / ArRest","Bit 3: Sig  / Dir","Bit 4: Power Off","Bit 5: Auto Power Off"]')
        self.addTo('')
        self.addTo('')
        self.addTo('# following values are a dump of the MoCon settings')
        self.addTo('# basic settings: JERK:                  3930..2147483647 (unit: revolution per minute^3);')
        self.addTo('#                                        0 means value is not required for that motor configuration')
        self.addTo('#                 PROFILE:               0=trapeze, 2=Constant Velocity, 2=s-curve')
        self.addTo('#                 RESOLUTION:            steps per revolution 1..8192')        
        self.addTo('#                 VELOCITY.ACCELERATION: 1717..2147483647 (unit: revolution per minute^2);')
        self.addTo('#                                        0 means value is not required for that motor configuration')        
        self.addTo('#                 VELOCITY.DECELERATION: 1717..2147483647 (unit: revolution per minute^2);')
        self.addTo('#                                        0 means value is not required for that motor configuration')        
        self.addTo('#                 VELOCITY.START:        this settings is only available for stepper motors')
        self.addTo('#                                        0.0115..81916250.0 (unit: revolution per minute);')
        self.addTo('#                                        0 means value is not required for that motor configuration')
        self.addTo('#                 VELOCITY.STRAIGHT:     1.55..81916250.0 (unit: revolution per minute);')
        self.addTo('BASIC.JERK:U32                  = ' + self.basicSettings.split('\r\n')[6].split(' ')[5].ljust(10) + '  +MIN:U32=0     +MAX:U32=2147483647      +UNIT:String=rev/min^3')
        self.addTo('BASIC.PROFILE:U16               = ' + self.basicSettings.split('\r\n')[0].split(' ')[5].ljust(10) + '  +SELECT:SeqString=[Trapeze,"Constant Velocity",S-Curve]')
        self.addTo('BASIC.RESOLUTION:U16            = ' + self.basicSettings.split('\r\n')[1].split(' ')[5].ljust(10) + '  +MIN:U16=1     +MAX:U16=8192            +UNIT:String=counts/rev')
        self.addTo('BASIC.VELOCITY.ACCELERATION:U32 = ' + self.basicSettings.split('\r\n')[4].split(' ')[5].ljust(10) + '  +MIN:U32=0     +MAX:U32=2147483647      +UNIT:String=rev/min^2')
        self.addTo('BASIC.VELOCITY.DECELERATION:U32 = ' + self.basicSettings.split('\r\n')[5].split(' ')[5].ljust(10) + '  +MIN:U32=0     +MAX:U32=2147483647      +UNIT:String=rev/min^2')
        self.addTo('BASIC.VELOCITY.START:Double     = ' + self.basicSettings.split('\r\n')[3].split(' ')[5].ljust(10) + '  +MIN:Double=0  +MAX:Double=8.19162e+07  +UNIT:String=rev/min')
        self.addTo('BASIC.VELOCITY.STRAIGHT:Double  = ' + self.basicSettings.split('\r\n')[2].split(' ')[5].ljust(10) + '  +MIN:Double=0  +MAX:Double=8.19162e+07  +UNIT:String=rev/min')
        self.addTo('')        
        self.addTo('')
        self.addTo('# encoder settings:             TYPE:                absolute and incremental encoder position value will be downloaded when the property is configured in that way.')
        self.addTo('#                                                    0=without encoder, 1=incremental encoder, 2=absolute encoder, 3=incremental and absolute encoder')
        self.addTo('# absolute encoder settings:    ABSOLUTE.CODE:       coding of the absolute encoder position')
        self.addTo('#                               ABSOLUTE.RESOLUTION: for absolute encoder 13..29 bit; default 25')
        self.addTo('# incremental encoder settings: INCREMENTAL.STEPS:   steps per revolution (only for stepper) -32767..32767')
        self.addTo('#                               INCREMENTAL.COUNTS:  incremental counts per revolution -32767..32767')
        self.addTo('#                               INCREMENTAL.ERROR:   error limit: 0..2147483647')
        self.addTo('ENCODER.TYPE:Int                    = ' + str(self.encoderType).ljust(10) +                               '  +SELECT:SeqString=["No Encoder","Incremental Encoder","Absolute Encoder","Incremental and Absolute Encoder"]')
        self.addTo('ENCODER.ABSOLUTE.CODE:U16           = 1           +MIN:U16=0         +MAX:U16=1')
        self.addTo('ENCODER.ABSOLUTE.RESOLUTION:U16     = 25          +MIN:U16=13        +MAX:U16=29          +UNIT:String=bit')
        self.addTo('ENCODER.INCREMENTAL.STEPS:Short     = ' + self.encoderSettings.split('\r\n')[1].split(' ')[5].ljust(10) + '  +MIN:Short=-32767  +MAX:Short=32767     +UNIT:String=steps/rev')
        self.addTo('ENCODER.INCREMENTAL.COUNTS:Short    = ' + self.encoderSettings.split('\r\n')[0].split(' ')[5].ljust(10) + '  +MIN:Short=-32767  +MAX:Short=32767     +UNIT:String=counts/rev')
        self.addTo('ENCODER.INCREMENTAL.ERROR_LIMIT:U32 = ' + self.encoderSettings.split('\r\n')[2].split(' ')[5].ljust(10) + '  +MIN:U32=0         +MAX:U32=2147483647')
        self.addTo('')
        self.addTo('')
        self.addTo('# firmware settings can not be downloaded. Properties are defined with default values!')
        self.addTo('# firmware settings: these values are stored in the EEPROM when uploaded and are valid after a reset.')
        self.addTo('#                    AUTOLOADER: if the flag is true the motor settings are loaded from the EEPROM automatically after a reset.')
        self.addTo('#                    EVENT:      if the flag is false all event messages are blocked.')
        self.addTo('#                    INFO:       if the flag is false all info messages are blocked.')
        self.addTo('FIRMWARE.AUTOLOADER:Bool = 0')
        self.addTo('FIRMWARE.EVENT:Bool      = 1')
        self.addTo('FIRMWARE.INFO:Bool       = 0')
        self.addTo('')
        self.addTo('')
        self.addTo('# motor settings: DOCKING_DISTANCE: to reference switch 1..1000000000')
        self.addTo('#                 REFERENCE_SENSE:  false=reference switch high active,  true=reference switch low active')
        self.addTo('#                 REFERENCE_SWITCH: 0=disable(default),')
        self.addTo('#                                   1=positive limit switch,')
        self.addTo('#                                   2=negative limit switch,')
        self.addTo('#                                   3=separate switch,')
        self.addTo('#                                   4=level switch')
        self.addTo('#                 TYPE:             0=servo, 1 stepper')
        self.addTo('#                 UNIT:             this is used to define the units of the position properties. Stepper: steps / SERVO: counts')
        self.addTo('#                 VELOCITY.DOCKING: home position docking velocity: 0.0115..81916250.0 (unit: revolution per minute);')
        self.addTo('#                                   0 means value is not required for that motor configuration')   
        self.addTo('#                 VELOCITY.HOME:    0.0115..81916250.0 (unit: revolution per minute);')
        self.addTo('#                                   0 means value is not required for that motor configuration')   
        self.addTo('MOTOR.DOCKING_DISTANCE:U32    = ' + self.motorSettings.split('\r\n')[4].split(' ')[5].ljust(10) + '  +MIN:U32=0     +MAX:U32=1000000000')
        self.addTo('MOTOR.REFERENCE.SENSE:Bool    = ' + self.motorSettings.split('\r\n')[1].split(' ')[5].ljust(10) + '  +MIN:U32=0     +MAX:U32=1               +SELECT:SeqString=["High Active","Low Active"]')
        self.addTo('MOTOR.REFERENCE.SWITCH:Int    = ' + self.motorSettings.split('\r\n')[0].split(' ')[5].ljust(10) + '  +MIN:U32=0     +MAX:U32=4               +SELECT:SeqString=["Disable (default)","Positive Limit Switch","Negative Limit Switch","Separate Switch","Level Switch"]')
        self.addTo('MOTOR.TYPE:Int                = ' + str(1 if (self.stepperSettings != None) else 0)             + '  +SELECT:SeqString=[Servo,Stepper]')
        self.addTo('MOTOR.UNIT:String             = ' + ('steps' if (self.stepperSettings != None) else 'counts'))
        self.addTo('MOTOR.VELOCITY.DOCKING:Double = ' + self.motorSettings.split('\r\n')[3].split(' ')[5].ljust(10) + '  +MIN:Double=0  +MAX:Double=8.19162e+07  +UNIT:String=rev/min')
        self.addTo('MOTOR.VELOCITY.HOME:Double    = ' + self.motorSettings.split('\r\n')[2].split(' ')[5].ljust(10) + '  +MIN:Double=0  +MAX:Double=8.19162e+07  +UNIT:String=rev/min')

        if (self.servoSettings != None):
            self.addTo('')
            self.addTo('')
            self.addTo('# servo loop settings: BIAS:  -100..100')
            self.addTo('#                      ILIM:  0..2147483647')
            self.addTo('#                      KAFF:  0..32767')
            self.addTo('#                      KD:    0..32767')
            self.addTo('#                      KI:    0..32767')
            self.addTo('#                      KOUT:  0..100')
            self.addTo('#                      KP:    0..32767')
            self.addTo('#                      KVFF:  0..32767')
            self.addTo('#                      Limit: 0..100')
            self.addTo('SERVOLOOP.BIAS:Short = ' + self.servoSettings.split('\r\n')[7].split(' ')[5].ljust(10) + '  +MIN:Short=-100  +MAX:Short=100       +UNIT:String=%')
            self.addTo('SERVOLOOP.ILIM:U32   = ' + self.servoSettings.split('\r\n')[6].split(' ')[5].ljust(10) + '  +MIN:U32=0       +MAX:U32=2147483647')
            self.addTo('SERVOLOOP.KAFF:U16   = ' + self.servoSettings.split('\r\n')[3].split(' ')[5].ljust(10) + '  +MIN:U16=0       +MAX:U16=32767     ')
            self.addTo('SERVOLOOP.KD:U16     = ' + self.servoSettings.split('\r\n')[2].split(' ')[5].ljust(10) + '  +MIN:U16=0       +MAX:U16=32767     ')
            self.addTo('SERVOLOOP.KI:U16     = ' + self.servoSettings.split('\r\n')[1].split(' ')[5].ljust(10) + '  +MIN:U16=0       +MAX:U16=32767     ')
            self.addTo('SERVOLOOP.KOUT:U16   = ' + self.servoSettings.split('\r\n')[5].split(' ')[5].ljust(10) + '  +MIN:U16=0       +MAX:U16=100         +UNIT:String=%')
            self.addTo('SERVOLOOP.KP:U16     = ' + self.servoSettings.split('\r\n')[0].split(' ')[5].ljust(10) + '  +MIN:U16=0       +MAX:U16=32767     ')
            self.addTo('SERVOLOOP.KVFF:U16   = ' + self.servoSettings.split('\r\n')[4].split(' ')[5].ljust(10) + '  +MIN:U16=0       +MAX:U16=32767     ')
            self.addTo('SERVOLOOP.LIMIT:U16  = ' + self.servoSettings.split('\r\n')[8].split(' ')[5].ljust(10) + '  +MIN:U16=0       +MAX:U16=100         +UNIT:String=%')
        if (self.stepperSettings != None):
            self.addTo('')
            self.addTo('')
            self.addTo('# stepper settings: MICROSTEPS.VALUE:    microsteps itself')
            self.addTo('#                   MICROSTEPS.ADAPTION: microsteps adaption 0=disabled; 1=enabled')
            self.addTo('#                   AUTO_POWER_OFF:      auto power off 0=disabled; 1=enabled')
            self.addTo('STEPPER.MICROSTEPS.VALUE:U32     = ' + self.stepperSettings.split('\r\n')[0].split(' ')[5].ljust(10))
            self.addTo('STEPPER.MICROSTEPS.ADAPTION:Bool = ' + self.stepperSettings.split('\r\n')[1].split(' ')[5].ljust(10))
            self.addTo('STEPPER.AUTO_POWER_OFF:Bool      = ' + self.stepperSettings.split('\r\n')[2].split(' ')[5].ljust(10))
                    
        f = open(self.path + "/" + self.filename, 'w')
        f.write(self.configFile)
        f.close()        

        
## Scan the TAN application file and construct the TAN configuration by merge with a data sheet file.
# In particular the VENDOR, TYPE and PRODUCT are set by the application file.
# Then the FILENAME points to a data sheet with the electronics parameters.
# More parameters (motor, encoder, servo loop settings) are queried from the electronics.
# Finally that fuller set of parameters is dumped to a Basda configuration file.
def main():
    try:
        initApplication()
        config = Application.config()

        if config.exist("V"):
            verbose = config.node("V").Int
        else:
            verbose = False

        if config.exist("TYPE"):
            type = config.node("TYPE").String
        else:
            type = "not defined"
        
        if config.exist("VENDOR"):
            vendor = config.node("VENDOR").String
        else:
            vendor = "not defined"
        
        if config.exist("PRODUCT"):
            product = config.node("PRODUCT").String
        else:
            product = "not defined"
        
        if config.exist("ENCODER.TYPE"):
            encoderType = config.node("ENCODER.TYPE").Int
        else:
            encoderType = 1
        
        if config.exist("FILENAME"):
            file = config.node("FILENAME").String
        else:
            file = config.node("CONFIG").String
            index = config.node("CONFIG").String.find(".cfg")
            file = file[:index] + "_" + config.node("MODULE").String + "_datasheet" + file[index:]
            
        if config.exist("DIR"):
            path = config.node("DIR").String
        else:
            for p in os.environ["INSROOT_ETC_PATH"].split(os.pathsep):
                if os.path.exists(p + config.node("CONFIG").String):
                    path = p
                    break            
            
        if config.exist("MODULE"):
            dump = Dump(config.node("MOD." + config.node("MODULE").String), type, vendor, product, encoderType, file, path, verbose)
            dump.connect()
            dump.reserveCard()
            dump.disableInfoMessageFlag()
            dump.readConnectionTimeout()
            dump.freeCard()
            dump.reserveModule()
            dump.now()
            dump.freeModule()
            dump.disconnect()
            dump.writeConfigFile()
        else:
            raise ERR_MODULE_NOT_DEFINED("MODULE node is missing!")
        N_LOG('dump done.')
    except Exception as e:
        E_LOG(e)
        N_LOG("Usage:")
        N_LOG("NOTE: the DEVICE config file is required and the module node;")
        N_LOG("      optional: ENCODER.TYPE [0='No Encoder',1='Incremental Encoder (default)',2='Absolute Encoder',3='Incremental and Absolute Encoder']")
        N_LOG("      optional: TYPE product type e.g. \\\"linear\ stage\\\" (you have to escaping double quote signs and spaces!)")
        N_LOG("      optional: VENDOR product manufacture")
        N_LOG("      optional: PRODUCT = product name") 
        N_LOG("      optional: FILENAME: for overwriting config file name")
        N_LOG("      optional: DIR: directory where to store the config file")
        N_LOG("      optional: V (verbose)")
        N_LOG(__file__ + " --CONFIG=path/to/config-dev.cfg --MODULE=X  --V=1   ")
        N_LOG(__file__ + " --CONFIG=laos/dx/hws/se/laos.dx.hws.se.starEnlarger01-dev.cfg --TYPE=\\\"linear\ stage\\\" --VENDOR=PI --PRODUCT=M505 --ENCODER.TYPE=1 --MODULE=X --V=1 ")
        N_LOG(__file__ + " --CONFIG=laos/dx/hws/se/laos.dx.hws.se.starEnlarger01-dev.cfg --TYPE=\\\"linear\ stage\\\" --VENDOR=PI --PRODUCT=M505 --ENCODER.TYPE=1 --MODULE=X --FILENAME=laos/dx/hws/se/laos.dx.hws.se.starEnlarger01-dev_X_datasheet.cfg --V=1 ")
    except SystemError as e:
        E_LOG("Config file not found.")
    
    except EnvironmentError as e:
        E_LOG(e)


if __name__ == "__main__":
    main()
