# Copyright 2015-2020 - RoboDK Inc. - https://robodk.com/
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# ----------------------------------------------------
# This file is a POST PROCESSOR for Robot Offline Programming to generate programs 
# for a KUKA robot using RoboDK. This post includes support for KRC2 and KRC4 controller
#
# To edit/test this POST PROCESSOR script file:
# Select "Program"->"Add/Edit Post Processor", then select your post or create a new one.
# You can edit this file using any text editor or Python editor. Using a Python editor allows to quickly evaluate a sample program at the end of this file.
# Python should be automatically installed with RoboDK
#
# You can also edit the POST PROCESSOR manually:
#    1- Open the *.py file with Python IDLE (right click -> Edit with IDLE)
#    2- Make the necessary changes
#    3- Run the file to open Python Shell: Run -> Run module (F5 by default)
#    4- The "test_post()" function is called automatically
# Alternatively, you can edit this file using a text editor and run it with Python
#
# To use a POST PROCESSOR file you must place the *.py file in "C:/RoboDK/Posts/"
# To select one POST PROCESSOR for your robot in RoboDK you must follow these steps:
#    1- Open the robot panel (double click a robot)
#    2- Select "Parameters"
#    3- Select "Unlock advanced options"
#    4- Select your post as the file name in the "Robot brand" box
#
# To delete an existing POST PROCESSOR script, simply delete this file (.py file)
#
# ----------------------------------------------------
# More information about RoboDK Post Processors and Offline Programming here:
#     https://robodk.com/help#PostProcessor
#     https://robodk.com/doc/en/PythonAPI/postprocessor.html
# ----------------------------------------------------


# ----------------------------------------------------
# Import RoboDK tools
from robodk import *


# Change the main header here
DEFAULT_HEADER = '''

GLOBAL INTERRUPT DECL 3 WHEN $STOPMESS==TRUE DO IR_STOPM ( )
INTERRUPT ON 3

;FOLD Initialise and set default speed
BAS (#INITMOV,0)
BAS (#VEL_PTP,100)
BAS (#ACC_PTP,20)
$VEL.CP=0.2
BAS (#TOOL,0)
BAS (#BASE,0)
;ENDFOLD

;;FOLD STARTPOS
;$BWDSTART = FALSE
;PDAT_ACT = PDEFAULT
;BAS(#PTP_DAT)
;FDAT_ACT = {TOOL_NO 0,BASE_NO 0,IPO_FRAME #BASE}
;BAS(#FRAMES)
;;ENDFOLD

$ADVANCE = 5

;FOLD ---- Quickly skip BCO ----
; PTP $AXIS_ACT 
;ENDFOLD

;FOLD ---- GO HOME ----
; PTP {A1 0.000, A2 -90.000, A3 90.000, A4 0.000, A5 0.000, A6 0.000, E1 0, E2 0, E3 0, E4 0, E5 0, E6 0}
;ENDFOLD

'''

DEFAULT_CODE_TOOL_ON = '''
;FOLD TURN_ON WELDING
;--- WELDING TURN_ON ---
$OUT[1] = TRUE
;---CONFIRM WELDING ON ---
WAIT FOR $IN[1] == TRUE
;--- PLASMA WELDING_ON ---
;ENDFOLD
'''

DEFAULT_CODE_TOOL_OFF = '''
;FOLD TURN_OFF WELDING
;--- WELDING TURN_OFF ---
$OUT[1] = FALSE
;---CONFIRM WELDING OFF ---
WAIT FOR $IN[1] == FALSE
WAIT SEC 1
;--- WELDING TURN_FALSE ---
;ENDFOLD
'''

    
# ----------------------------------------------------    
# Object class that handles the robot instructions/syntax
# Important: This is the default KUKA KRC2 Post used as a reference for all other posts
class RobotPost(object):
    """Robot post object"""  
        
    # ------------------------------------------------------------------------
    # ------------------- KUKA POST PROCESSOR CONFIGURATION ------------------
    # Maximum number of lines per program. It will then generate multiple "pages (files)" if the program is too long
    # This is the default value. You can change this value in RoboDK:
    # Tools-Options-Program-Limit the maximum number of lines per program
    MAX_LINES_X_PROG = 2500  

    # Set to True to include subprograms in the main program. Otherwise, programs can be generated separately.
    INCLUDE_SUB_PROGRAMS = False # Set to True to include subprograms in the same file
    
    # Generate subprograms as new files instead of adding them in the same file (INCLUDE_SUB_PROGRAMS must be set to True)
    SUB_PROGRAMS_AS_FILES = True

    # Set KRC Version (2 or 4)
    # Version 2 for KRC2 controllers or Version 4 for KRC4
    KRC_VERSION = 2  # for KRC2 controllers
    #KRC_VERSION = 4  # for KRC4 controllers
    
    # Display messages on the teach pendant. This feature is not supported by all controllers and must be disabled for some controllers.
    SKIP_MESSAGE_POPUPS = False
    #SKIP_MESSAGE_POPUPS = True    
    
    # Use frame index if provided (example: rename your reference to Frame 2 to use BASE_DATA[2])
    FRAME_INDEX = False
    
    # Use Tool index if provided (example: rename your tool to Tool 3 to use TOOL_DATA[3])
    TOOL_INDEX = False

    # Set if we want to have cascaded program calls when program splitting takes place
    # Usually, cascaded is better for older controllers
    CASCADED_CALLS = (KRC_VERSION <= 2)
    # CASCADED_CALLS = True
    # CASCADED_CALLS = False

    # Optionally output configuration and turn flags S and T
    # If we set this to true, the output targets will have the configuration and turn bits added in the movement 
    # (for example: S B'010', T B'000001')
    ADD_CONFIG_TURN = False
    
    # Add a keyword to split subprograms
    # SKIP_PROG_KEYWORD = 'spindle'
    SKIP_PROG_KEYWORD = None

    # File extension: set the program extension
    PROG_EXT = 'src'

    # Program name max length (KRC4 is 24 characters)
    PROG_NAME_LEN = 16

    # ------- Parameters for 3D printing --------
    # Use a synchronized external axis as an extruder (E1, E2, ... or the last available axis)
    #EXTAXIS_EXTRUDER = True
    EXTAXIS_EXTRUDER = False

    # Use a weld gun as an extruder
    #WELD_EXTRUDER = True
    WELD_EXTRUDER = False

    # Parameters for 3D printing using a custom extruder
    # 3D Printing Extruder Setup Parameters:
    PRINT_E_ANOUT = 5              # Analog Output ID to command the extruder flow
    PRINT_SPEED_2_SIGNAL = 0.10 # Ratio to convert the speed/flow to an analog output signal
    PRINT_FLOW_MAX_SIGNAL = 24  # Maximum signal to provide to the Extruder
    PRINT_ACCEL_MMSS = -1      # Acceleration, -1 assumes constant speed if we use rounding/blending

    # ----------------------------------------------------  

    # Change the main header here
    HEADER = DEFAULT_HEADER

    CODE_TOOL_ON = DEFAULT_CODE_TOOL_ON

    CODE_TOOL_OFF = DEFAULT_CODE_TOOL_OFF
    
    # Set the order of external axes
    AXES_DATA = ['A1','A2','A3','A4','A5','A6','E1','E2','E3','E4','E5','E6']
    
    #-----------------------------------------------------------
    #-----------------------------------------------------------
    # ----------------------------------------------------
    def filtername_len(self, name):
        return FilterName(name[:self.PROG_NAME_LEN], 'P')
        
    def pose_2_str(self, pose):
        """Converts a pose target to a string"""
        if pose is None:
            raise Exception("Unable to create a Linear movement (LIN) using a Joint Target. Use a Cartesian target instead.")
        #[x,y,z,w,p,r] = Pose_2_KUKA(pose)
        #return ('X %.3f, Y %.3f, Z %.3f, A %.3f, B %.3f, C %.3f' % (x,y,z,w,p,r)) # old version had to be switched
        [x,y,z,r,p,w] = pose_2_xyzrpw(pose)
        return ('X %.3f,Y %.3f,Z %.3f,A %.3f,B %.3f,C %.3f' % (x,y,z,w,p,r))

    def pose_2_str_ext(self,pose,joints):
        njoints = len(joints)
        if njoints <= 6:
            return self.pose_2_str(pose)
        else:     
            extaxes = ''
            for i in range(njoints-6):
                extaxes = extaxes + (',%s %.5f' % (self.AXES_DATA[i+6], joints[i+6]))
            return self.pose_2_str(pose) + extaxes
        
    def angles_2_str(self,angles):
        """Prints a joint target"""
        str = ''
        #if self.KRC_VERSION >= 4:
        #    str = 'AXIS: '       
        
        for i in range(len(angles)):
            str = str + ('%s %.5f,' % (self.AXES_DATA[i], angles[i]))
        str = str[:-1]
        return str

          
    def conf_2_str(self,confRLF):
        if confRLF is None:
            return "'B010'"
        strconf = ""
        if confRLF[2] > 0:
            strconf = strconf + '1'
        else:
            strconf = strconf + '0'
            
        if confRLF[1] == 0:
            strconf = strconf + '1'
        else:
            strconf = strconf + '0'
            
        if confRLF[0] > 0:
            strconf = strconf + '1'
        else:
            strconf = strconf + '0'
        
        return "'B%s'" % strconf    
        
    def joints_2_turn_str(self,joints):
        if joints is None:
            return "'B000000'"
            
        strturn = ""
        for i in range(len(joints)):
            if joints[i] < 0:
                strturn = '1' + strturn
            else:
                strturn = '0' + strturn
        return "'B%s'" % strturn  
    
    #------------------------------------------------
    #------------------------------------------------
    #------------------------------------------------
    
    # other variables
    ROBOT_POST = ''
    ROBOT_NAME = ''
    lines_x_prog = MAX_LINES_X_PROG
    
    # Multiple program files variables
    PROG_NAME = 'unknown' # single program name
    PROG_NAMES = []
    PROG_FILES = []    
    PROG_LIST = []
    nLines = 0
    nProgs = 0
    AutoCreateMain = True
    
    PROG = []
    LOG = ''
    nAxes = 6
    APO_VALUE = 1
    C_DIS = ''#' C_DIS'
    C_PTP = ''#' C_PTP'
    
    PROG_CALLS = []
    PROG_CALLS_LIST = []    
    
    # Default speed
    SPEED_MMS = 500
    
    ARC_ON = False
       
    # Internal 3D Printing Parameters
    PRINT_POSE_LAST = None # Last pose printed
    PRINT_E_LAST = 0 # Last Extruder length
    PRINT_E_NEW = None # New Extruder Length
    PRINT_E_NEW_MOTOR = 0 # External axis value to start controlling the external axis
    PRINT_LAST_SIGNAL = None # Last extruder signal
    
    def __init__(self, robotpost=None, robotname=None, robot_axes = 6, **kwargs):
        self.ROBOT_POST = robotpost
        self.ROBOT_NAME = robotname
        self.PROG = []
        self.LOG = ''
        self.nAxes = robot_axes
        for k,v in kwargs.items():
            if k == 'lines_x_prog':
                self.lines_x_prog = v       
        
    def ProgStart(self, progname, new_page = False):
        progname = self.filtername_len(progname)
        progname_i = progname
        if new_page:
            nPages = len(self.PROG_LIST)
            if nPages == 0:
                progname_i = progname
            else:
                progname_i = "%s%i" % (self.PROG_NAME, nPages)
            
        else:
            #self.PROG_NAME = progname
            #self.nProgs = self.nProgs + 1
            #self.PROG_NAMES = []
            #if self.nProgs > 1 and not self.INCLUDE_SUB_PROGRAMS:
            #    return
        
            self.PROG_NAME = progname
            self.nProgs = self.nProgs + 1
            #self.PROG_NAMES = []
            if self.nProgs > 1:
                if not self.INCLUDE_SUB_PROGRAMS:
                    # Stop adding programs
                    return
                else:
                    self.AutoCreateMain = False
                    if not self.SUB_PROGRAMS_AS_FILES:
                        # Stop adding programs
                        self.PROG_NAMES = []
                    else:
                        # Important!! We can't have cascaded calls if we are including sub programs (one or the other)
                        print("Warning! Adding subprograms and automatic program splitting is not supported")                    

        self.PROG_NAMES.append(progname_i)    
    
        self.addline('DEF %s ( )' % progname_i)
        if not new_page:
            if self.KRC_VERSION < 4:
                # KRC2 needs this EXT declaration
                self.PROG.append('EXT BAS (BAS_COMMAND :IN,REAL :IN )');
                
            self.PROG.append(self.HEADER)
            #if self.nAxes > 6:
            #    self.addline('$ACT_EX_AX = %i' % (self.nAxes-6))
        #else:
        #    self.PROG += '*PROGDEFS*\n'
        
    #def ProgFinish(self, progname, new_page = False):        
    #    if new_page:
    #        self.PROG.append("END")
    #        self.PROG_LIST.append(self.PROG)
    #        self.PROG_CALLS_LIST.append(self.PROG_CALLS)
    #        self.PROG = []
    #        self.PROG_CALLS = []
    #        self.nLines = 0
    
    def ProgFinish(self, progname, new_page = False):        
        if new_page or (self.INCLUDE_SUB_PROGRAMS and self.SUB_PROGRAMS_AS_FILES):
            self.PROG.append("END")
            self.PROG_LIST.append(self.PROG)
            self.PROG_CALLS_LIST.append(self.PROG_CALLS)
            self.PROG = []
            self.PROG_CALLS = []
            self.nLines = 0
        
    def progsave(self, filesave): 
        #progname = progname + '.' + self.PROG_EXT                
        #filesave = folder + '/' + progname
        
        ext_defs = ''   
        if self.KRC_VERSION < 4:
            for prog_nm in self.PROG_CALLS:
                ext_defs += "EXT %s()\n" % prog_nm
        
        if len(ext_defs) > 0 and len(self.PROG) > 1:
            self.PROG.insert(1, ext_defs)
            self.PROG.insert(1, "\n; External program calls:")            
            
        with open(filesave, "w", encoding="utf-8") as fid:
            #fid.write("&ACCESS RVP\n")
            #fid.write("&REL 1\n")
            #fid.write("&COMMENT Generated by RoboDK\n")
            fid.write("&ACCESS RVP\n")
            fid.write("&REL 1\n") #fid.write("&REL 29\n")
            fid.write("&PARAM TEMPLATE = C:\\KRC\\Roboter\\Template\\vorgabe\n")
            fid.write("&PARAM EDITMASK = *\n")
            for line in self.PROG:
                fid.write(line)
                fid.write("\n")

        print('SAVED: %s\n' % filesave) # tell RoboDK the path of the saved file
        self.PROG_FILES.append(filesave)
        
    def ProgSave(self, folder, progname, ask_user = False, show_result = False):
        progname = self.filtername_len(progname)
        if ask_user or not DirExists(folder):
            folder = getSaveFolder(folder,'Select a directory to save your program')
            if folder is None:
                # The user selected the Cancel button
                return
        
        # Remember the files to display
        files_display = []           
        
        if len(self.PROG_LIST) >= 1:
            if self.nLines > 0:
                self.ProgFinish(progname, True)  

            npages = len(self.PROG_LIST)
            
            # Check if we are generating subprograms as files (in this case, automatic splitting is not supported)
            if self.AutoCreateMain:            
                if self.CASCADED_CALLS:                
                    for i in range(npages-1):
                        prog_call_next = self.PROG_NAMES[i+1]
                        self.PROG_CALLS_LIST[i].append(prog_call_next)
                        self.PROG_LIST[i].insert(-1, "\n; Continue running program as cascaded execution")
                        self.PROG_LIST[i].insert(-1, prog_call_next+"()\n")
                        
                else:
                    progname_main = progname + "Main"
                    mainprog = []
                    mainprog.append("DEF %s ( )" % progname_main)
                    for i in range(npages):
                        self.PROG = self.PROG_LIST[i]
                        mainprog.append("%s()" % self.PROG_NAMES[i])
                        
                    mainprog.append("END")
                    self.PROG = mainprog
                    filesave = folder + '/' + progname_main + '.' + self.PROG_EXT
                    files_display.append(filesave)
                    self.progsave(filesave)
                        
            for i in range(npages):
                self.PROG = self.PROG_LIST[i]
                self.PROG_CALLS = self.PROG_CALLS_LIST[i]
                filesave = folder + '/' + self.PROG_NAMES[i] + '.' + self.PROG_EXT
                files_display.append(filesave)
                self.progsave(filesave)
                
        else:
            self.PROG.append("END")
            filesave = folder + '/' + progname + '.' + self.PROG_EXT
            files_display.append(filesave)
            self.progsave(filesave)
            
            
        #-------------------------------------------------------        
        # open file with default application
        if show_result:
            # Do not display too many files
            if len(files_display) > 4:
                files_display = files_display[:4]
                
            if type(show_result) is str:
                # Open file with provided application
                import subprocess
                p = subprocess.Popen([show_result] + files_display)
            elif type(show_result) is list:
                import subprocess
                p = subprocess.Popen(show_result + files_display)   
            else:
                # open file with default application
                import os
                os.startfile(filesave)
            if len(self.LOG) > 0:
                mbox('Program generation LOG:\n\n' + self.LOG)    
    
    def ProgSendRobot(self, robot_ip, remote_path, ftp_user, ftp_pass):
        """Send a program to the robot using the provided parameters. This method is executed right after ProgSave if we selected the option "Send Program to Robot".
        The connection parameters must be provided in the robot connection menu of RoboDK"""
        UploadFTP(self.PROG_FILES, robot_ip, remote_path, ftp_user, ftp_pass)
            
    def calculate_time(self, distance, Vmax, Amax=-1):
        """Calculate the time to move a distance with Amax acceleration and Vmax speed"""
        if Amax < 0:
            # Assume constant speed (appropriate smoothing/rounding parameter must be set)
            Ttot = distance/Vmax
        else:
            # Assume we accelerate and decelerate
            tacc = Vmax/Amax;
            Xacc = 0.5*Amax*tacc*tacc;
            if distance <= 2*Xacc:
                # Vmax is not reached
                tacc = sqrt(distance/Amax)
                Ttot = tacc*2
            else:
                # Vmax is reached
                Xvmax = distance - 2*Xacc
                Tvmax = Xvmax/Vmax
                Ttot = 2*tacc + Tvmax
        return Ttot
            
    def new_move(self, new_pose):                        
        """Implement the action on the extruder for 3D printing, if applicable"""
        if self.WELD_EXTRUDER or self.EXTAXIS_EXTRUDER:
            return
            
        if self.PRINT_E_NEW is None or new_pose is None:
            return
            
        # Skip the first move and remember the pose
        if self.PRINT_POSE_LAST is None:
            self.PRINT_POSE_LAST = new_pose
            return          

        # Calculate the increase of material for the next movement
        add_material = self.PRINT_E_NEW - self.PRINT_E_LAST
        self.PRINT_E_LAST = self.PRINT_E_NEW
        
        # Calculate the robot speed and Extruder signal
        extruder_signal = 0
        if add_material > 0:
            distance_mm = norm(subs3(self.PRINT_POSE_LAST.Pos(), new_pose.Pos()))
            # Calculate movement time in seconds
            time_s = self.calculate_time(distance_mm, self.SPEED_MMS, self.PRINT_ACCEL_MMSS)
            
            # Avoid division by 0
            if time_s > 0:
                # This may look redundant but it allows you to account for accelerations and we can apply small speed adjustments
                speed_mms = distance_mm / time_s
                
                # Calculate the extruder speed in RPM*Ratio (self.PRINT_SPEED_2_SIGNAL)
                extruder_signal = speed_mms * self.PRINT_SPEED_2_SIGNAL
        
        # Make sure the signal is within the accepted values
        extruder_signal = max(0,min(self.PRINT_FLOW_MAX_SIGNAL, extruder_signal))
        
        # Update the extruder speed when required
        if self.PRINT_LAST_SIGNAL is None or abs(extruder_signal - self.PRINT_LAST_SIGNAL) > 1e-6:
            self.PRINT_LAST_SIGNAL = extruder_signal
            # Use the built-in setDO function to set an analog output
            #self.setDO(PRINT_E_ANOUT, "%.3f" % extruder_signal)
            # Use customized code for the output
            self.addline('$ANOUT[%i]=%.3f' % (self.PRINT_E_ANOUT, extruder_signal))
            # Use customized program call
            #self.addline('ExtruderSpeed(%.3f)' % extruder_signal)
        
        # Remember the last pose
        self.PRINT_POSE_LAST = new_pose
    
    def MoveJ(self, pose, joints, conf_RLF=None):
        """Add a joint movement"""
        if self.EXTAXIS_EXTRUDER:
            joints.append(self.PRINT_E_NEW_MOTOR)
            
        self.addline('PTP {' + self.angles_2_str(joints) + '}' + self.C_PTP)
        
    def MoveL(self, pose, joints, conf_RLF=None):
        """Add a linear movement"""
        self.new_move(pose) # used for 3D printing
        
        if self.EXTAXIS_EXTRUDER:
            joints.append(self.PRINT_E_NEW_MOTOR)
        
        # Optionally, add configuration data
        str_config = ''
        if self.ADD_CONFIG_TURN:
            str_config = ' S %s, T %s' % (self.conf_2_str(conf_RLF), self.joints_2_turn_str(joints))
            
        self.addline('LIN {' + self.pose_2_str_ext(pose,joints) + str_config + '}' + self.C_DIS)
        
    def MoveC(self, pose1, joints1, pose2, joints2, conf_RLF_1=None, conf_RLF_2=None):
        """Add a circular movement"""
        self.new_move(pose2) # used for 3D printing
        
        if self.EXTAXIS_EXTRUDER:
            joints2.append(self.PRINT_E_NEW_MOTOR)
        
        str_config1 = ''
        str_config2 = ''
        if self.ADD_CONFIG_TURN:
            str_config1 = ' S %s, T %s' % (self.conf_2_str(conf_RLF_1), self.joints_2_turn_str(joints))
            str_config2 = ' S %s, T %s' % (self.conf_2_str(conf_RLF_2), self.joints_2_turn_str(joints))
        
        self.addline('CIRC {' + self.pose_2_str_ext(pose1,joints1) + str_config1 + '},{' + self.pose_2_str_ext(pose2,joints2) + str_config2 + '}' + self.C_DIS)
        
    def setFrame(self, pose, frame_id, frame_name):
        """Change the robot reference frame"""
        self.addline('; ---- Setting reference (Base) ----')
        if self.nAxes > 6:
            # Code to output if the robot has an external axis:
            
            # option 1:
            #self.addline('$BASE = EK (MACHINE_DEF[2].ROOT, MACHINE_DEF[2].MECH_TYPE, { %s })' % self.pose_2_str(pose))    

            # option 2:            
            #self.addline('$BASE=EK(EX_AX_DATA[1].ROOT,EX_AX_DATA[1].EX_KIN, { %s })' % self.pose_2_str(pose))
            
            # Option 3:
            self.addline('; Using external axes')
            self.addline('; $BASE=EK(EX_AX_DATA[1].ROOT,EX_AX_DATA[1].EX_KIN,EX_AX_DATA[1].OFFSET)')
            self.addline('; $ACT_EX_AX= %i' % (self.nAxes - 6))
            #return
            
        if frame_name is not None and frame_name.endswith("Base"): 
            # Usually for the robot base frame
            frame_id = 1
            self.BASE_ID = frame_id
            self.addline('$BASE = {FRAME: ' + self.pose_2_str(pose) + '}')
            self.addline('; BASE_DATA[%i] = {FRAME: %s}' % (self.BASE_ID, self.pose_2_str(pose)))
            self.addline('; $BASE = BASE_DATA[%i]' % (self.BASE_ID))
            
        elif frame_id is not None and frame_id >= 1: 
            # specified ID reference frame (for example a reference frame named "Frame 2" -> frame_id = 2
            self.BASE_ID = frame_id
            self.addline('; BASE_DATA[%i] = {FRAME: %s}' % (self.BASE_ID, self.pose_2_str(pose)))            
            if self.FRAME_INDEX:
                self.addline('; $BASE = {FRAME: ' + self.pose_2_str(pose) + '}')            
                self.addline('$BASE = BASE_DATA[%i]' % (self.BASE_ID))
            else:
                self.addline('$BASE = {FRAME: ' + self.pose_2_str(pose) + '}')            
                self.addline('; $BASE = BASE_DATA[%i]' % (self.BASE_ID))
            
        else: 
            # unspecified ID reference frame
            self.BASE_ID = 1
            self.addline('$BASE = {FRAME: ' + self.pose_2_str(pose) + '}')
            
        self.addline('; --------------------------')
        
    def setTool(self, pose, tool_id, tool_name):
        """Change the robot TCP"""        
        self.addline('; ---- Setting tool (TCP) ----')
        
        if tool_id is not None and tool_id >= 1:
            # specified ID tool (for example, a tool named "Tool 3" -> tool_id = 3
            self.TOOL_ID = tool_id      
            self.addline('; TOOL_DATA[%i] = {FRAME: %s}' % (self.TOOL_ID, self.pose_2_str(pose)))                        
            if self.TOOL_INDEX:
                self.addline('; $TOOL = {FRAME: ' + self.pose_2_str(pose) + '}')            
                self.addline('$TOOL = TOOL_DATA[%i]' % (self.TOOL_ID))
            else:
                self.addline('$TOOL = {FRAME: ' + self.pose_2_str(pose) + '}')            
                self.addline('; $TOOL = TOOL_DATA[%i]' % (self.TOOL_ID))
            
        else:
            self.TOOL_ID = 1 # Default Tool ID
            self.addline('$TOOL = {FRAME: ' + self.pose_2_str(pose) + '}')
            
        self.addline('; --------------------------')
        
    def Pause(self, time_ms):
        """Pause the robot program"""
        if time_ms <= 0:
            self.addline('HALT')
        else:
            self.addline('WAIT SEC %.3f' % (time_ms*0.001))
        
    def setSpeed(self, speed_mms):
        """Changes the robot speed (in mm/s)"""
        self.SPEED_MMS = speed_mms
        self.addline('$VEL.CP = %.5f' % (speed_mms/1000.0))
    
    def setAcceleration(self, accel_mmss):
        """Changes the current robot acceleration"""
        self.addline('$ACC.CP = %.5f' % (accel_mmss/1000.0))
                
    def setSpeedJoints(self, speed_degs):
        """Changes the robot joint speed (in deg/s)"""
        self.addline('$VEL.ORI1 = %.5f' % speed_degs)
        self.addline('$VEL.ORI2 = %.5f' % speed_degs)
    
    def setAccelerationJoints(self, accel_degss):
        """Changes the robot joint acceleration (in deg/s2)"""
        self.addline('$ACC.ORI1 = %.5f' % accel_degss)
        self.addline('$ACC.ORI2 = %.5f' % accel_degss)
        
    def setZoneData(self, zone_mm):
        """Changes the zone data approach (makes the movement more smooth)"""
        self.APO_VALUE = zone_mm
        if zone_mm >= 0:
            self.addline('$APO.CPTP = %.3f' % min(100,zone_mm))
            self.addline('$APO.CDIS = %.3f' % zone_mm)
            self.C_DIS = ' C_DIS'
            self.C_PTP = ' C_PTP'
        else:
            self.C_DIS = ''
            self.C_PTP = ''
        
    def setDO(self, io_var, io_value):
        """Set a Digital Output"""
        if type(io_var) != str:  # set default variable name if io_var is a number
            io_var = '$OUT[%s]' % str(io_var)        
        if type(io_value) != str: # set default variable value if io_value is a number            
            if io_value > 0:
                io_value = 'TRUE'
            else:
                io_value = 'FALSE'
        
        # at this point, io_var and io_value must be string values
        self.addline('%s=%s' % (io_var, io_value))
        
    def setAO(self, io_var, io_value):
        """Set an Analog Output"""
        if type(io_var) != str:
            io_var = '$ANOUT[%s]' % str(io_var)
            
        self.setDO(io_var, io_value)
        
    def waitDI(self, io_var, io_value, timeout_ms=-1):
        """Waits for an input io_var to attain a given value io_value. Optionally, a timeout can be provided."""
        if type(io_var) != str:  # set default variable name if io_var is a number
            io_var = '$IN[%s]' % str(io_var)        
        if type(io_value) != str: # set default variable value if io_value is a number            
            if io_value > 0:
                io_value = 'TRUE'
            else:
                io_value = 'FALSE'
        
        # at this point, io_var and io_value must be string values
        if timeout_ms < 0:
            self.addline('WAIT FOR (%s==%s)' % (io_var, io_value))
        else:
            self.addline('START_TIMER:')
            self.addline('$TIMER_STOP[1]=TRUE')
            self.addline('$TIMER_FLAG[1]=FALSE')
            self.addline('$TIMER[1]=%.3f' % (float(timeout_ms)*0.001))
            self.addline('$TIMER_STOP[1]=FALSE')
            self.addline('WAIT FOR (%s==%s OR $TIMER_FLAG[1])' % (io_var, io_value))
            self.addline('$TIMER_STOP[1]=TRUE')
            self.addline('IF $TIMER_FLAG[1]== TRUE THEN')
            self.addline('    HALT ; Timed out!')
            self.addline('    GOTO START_TIMER')
            self.addline('ENDIF')      
        
    def RunCode(self, code, is_function_call = False):
        """Adds code or a function call"""
        if self.SKIP_PROG_KEYWORD is not None and self.SKIP_PROG_KEYWORD.lower() in code.lower():
            self.addline(';' + code)
            return
            
        if is_function_call:
            code_lower = code.lower()
            if code_lower.startswith("extruder("):            
                # If the program call is Extruder(123.56), we extract the number as a string and convert it to a number                
                e_new = float(code[9:-1]) # it needs to retrieve the extruder length from the program call
                
                if e_new > 0.0 and not self.ARC_ON:
                    if self.WELD_EXTRUDER:
                        self.ARC_ON = True
                        self.addline(self.CODE_TOOL_ON)
                    
                elif e_new <= 0.0 and self.ARC_ON:
                    if self.WELD_EXTRUDER:
                        self.ARC_ON = False
                        self.addline(self.CODE_TOOL_OFF)
                    
                #print("Extruder: %.3f" % e_new)
                # Do not generate the program call
                self.PRINT_E_NEW = e_new
                self.PRINT_E_NEW_MOTOR += max(0,self.PRINT_E_NEW-self.PRINT_E_LAST)
                #self.addline('Print: E %.3f' % self.PRINT_E_NEW)
                return
                
            elif code_lower.startswith('arc_on') or code_lower.startswith('arcstart'):
                if not self.ARC_ON:
                    self.ARC_ON = True
                    self.addline(self.CODE_TOOL_ON)
                return
                
            elif code_lower.startswith('arc_off') or code_lower.startswith('arcend'):
                if self.ARC_ON:
                    self.ARC_ON = False
                    self.addline(self.CODE_TOOL_OFF)
                return
        
            code = code.replace(' ','_')
            if not code.endswith(')'):
                code = self.filtername_len(code)
                code = code + '()'
                
            fcn_def = code
            if '(' in fcn_def:
                fcn_def = fcn_def.split('(')[0]
            if not (fcn_def in self.PROG_CALLS):
                self.PROG_CALLS.append(fcn_def)
                
            self.addline(code)
        else:
            self.addline(code)
        
    def RunMessage(self, message, iscomment = False):
        """Add a comment or a popup message"""
        if iscomment or self.SKIP_MESSAGE_POPUPS:
            self.addline('; ' + message)
        else:
            self.addline('Wait for StrClear($LOOP_MSG[])')
            self.addline('$LOOP_CONT = TRUE')
            self.addline('$LOOP_MSG[] = "%s"' % message)
            
# ------------------ private ----------------------                       
    def addline(self, newline):
        """Add a program line"""
        if self.nProgs > 1 and not self.INCLUDE_SUB_PROGRAMS:
            return
        
        if self.nLines > self.lines_x_prog:
            self.nLines = 0
            self.ProgFinish(self.PROG_NAME, True)
            self.ProgStart(self.PROG_NAME, True)

        self.PROG.append(newline)
        self.nLines = self.nLines + 1
        
    def addlog(self, newline):
        """Add a log message"""
        if self.nProgs > 1 and not self.INCLUDE_SUB_PROGRAMS:
            return
            
        self.LOG = self.LOG + newline + '\n'

# -------------------------------------------------
# ------------ For testing purposes ---------------   
def Pose(xyzrpw):
    [x,y,z,r,p,w] = xyzrpw
    a = r*math.pi/180
    b = p*math.pi/180
    c = w*math.pi/180
    ca = math.cos(a)
    sa = math.sin(a)
    cb = math.cos(b)
    sb = math.sin(b)
    cc = math.cos(c)
    sc = math.sin(c)
    return Mat([[cb*ca, ca*sc*sb - cc*sa, sc*sa + cc*ca*sb, x],[cb*sa, cc*ca + sc*sb*sa, cc*sb*sa - ca*sc, y],[-sb, cb*sc, cc*cb, z],[0,0,0,1]])

def test_post():
    """Test the post with a basic program"""

    robot = RobotPost('Kuka_custom', 'Generic Kuka')

    robot.ProgStart("Program")
    robot.RunMessage("Program generated by RoboDK", True)
    robot.setFrame(Pose([807.766544, -963.699898, 41.478944, 0, 0, 0]), 4, "Frame 4")
    robot.setTool(Pose([62.5, -108.253175, 100, -60, 90, 0]), 2, "Tool 2")
    robot.MoveJ(Pose([200, 200, 500, 180, 0, 180]), [-46.18419, -6.77518, -20.54925, 71.38674, 49.58727, -302.54752] )
    robot.MoveL(Pose([200, 250, 348.734575, 180, 0, -150]), [-41.62707, -8.89064, -30.01809, 60.62329, 49.66749, -258.98418] )
    robot.MoveL(Pose([200, 200, 262.132034, 180, 0, -150]), [-43.73892, -3.91728, -35.77935, 58.57566, 54.11615, -253.81122] )
    robot.RunMessage("Setting air valve 1 on")
    robot.RunCode("ArcStart(1)", True)
    robot.Pause(1000)
    robot.MoveL(Pose([200, 250, 348.734575, 180, 0, -150]), [-41.62707, -8.89064, -30.01809, 60.62329, 49.66749, -258.98418] )
    robot.MoveL(Pose([250, 300, 278.023897, 180, 0, -150]), [-37.52588, -6.32628, -34.59693, 53.52525, 49.24426, -251.44677] )
    robot.MoveL(Pose([250, 250, 191.421356, 180, 0, -150]), [-39.75778, -1.04537, -40.37883, 52.09118, 54.15317, -246.94403] )
    robot.RunMessage("Setting air valve off")
    robot.RunCode("ArcEnd()", True)
    robot.Pause(1000)
    robot.MoveL(Pose([250, 300, 278.023897, 180, 0, -150]), [-37.52588, -6.32628, -34.59693, 53.52525, 49.24426, -251.44677] )
    robot.MoveL(Pose([250, 200, 278.023897, 180, 0, -150]), [-41.85389, -1.95619, -34.89154, 57.43912, 52.34162, -253.73403] )
    robot.MoveL(Pose([250, 150, 191.421356, 180, 0, -150]), [-43.82111, 3.29703, -40.29493, 56.02402, 56.61169, -249.23532] )
    robot.ProgFinish("Program")
    # robot.ProgSave(".","Program",True)
    for line in robot.PROG:
        print(line)
        
    if len(robot.LOG) > 0:
        mbox('Program generation LOG:\n\n' + robot.LOG)

    input("Press Enter to close...")

if __name__ == "__main__":
    """Function to call when the module is executed by itself: test"""
    test_post()
