# ---------------------------------------------------------------------------------
#
# Copyright 2024 - 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 is a Python module that allows driving a Fairino cobot.
# This Python module can be run directly in console mode to test its functionality.
# This module allows communicating with a robot through the command line.
# The same commands we can input manually are used by RoboDK to drive the robot from the PC.
# RoboDK Drivers are located in /RoboDK/api/Robot/ by default. Drivers can be PY files or EXE files.
#
# Drivers are modular. They are not part of the RoboDK executable but they must be placed in C:/RoboDK/api/robot/, then, linked in the Connection parameters menu:
#   1. right click a robot in RoboDK, then, select "Connect to robot".
#   2. In the "More options" menu it is possible to update the location and name of the driver.
# Driver linking is automatic for currently available drivers.
# More information about robot drivers available here:
#   https://robodk.com/doc/en/Robot-Drivers.html#RobotDrivers
#
# Alternatively to the standard programming methods (where a program is generated, then, transferred to the robot and executed) it is possible to run a program simulation directly on the robot
# The robot movement in the simulator is then synchronized with the real robot.
# Programs generated from RoboDK can be run on the robot by right clicking the program, then selecting "Run on robot".
#   Example:
#   https://www.youtube.com/watch?v=pCD--kokh4s
#
# Example of an online programming project:
#   https://robodk.com/blog/online-programming/
#
# It is possible to control the movement of a robot from the RoboDK API (for example, from a Python or C# program using the RoboDK API).
# The same code is used to simulate and optionally move the real robot.
#   Example:
#   https://robodk.com/offline-programming
#
#   To establish connection from RoboDK API:
#   https://robodk.com/doc/en/PythonAPI/robolink.html#robolink.Item.ConnectSafe
#
# Example of a quick manual test in console mode:
#  User entry: CONNECT 192.168.123.1 7000
#  Response:   SMS:Response from the robot or failure to connect
#  Response:   SMS:Ready
#  User entry: MOVJ 10 20 30 40 50 60 70
#  Response:   SMS:Working...
#  Response:   SMS:Ready
#  User entry: CJNT
#  Response:   SMS:Working...
#  Response:   JNTS: 10 20 30 40 50 60 70
#
# ---------------------------------------------------------------------------------

DRIVER_VERSION = "Fairino RoboDK Driver v0.0.1"

import sys
import atexit
import threading
from queue import Queue
import time



global LOCK_STDOUT
LOCK_STDOUT = threading.Lock()


# Note, a simple print() will flush information to the log window of the robot connection in RoboDK
# Sending a print() might not flush the standard output unless the buffer reaches a certain size
# IMPORTANT: Don't use print() in your code, always use a wrapper function below or the driver might misbehave

def print_status(status):
    """
    Display a message in the connection log window and the connection status bar of RoboDK (SMS:).
    The status bar has coloring, for example, \"Ready\" will be displayed in green, \"Waiting\" in blue, \"Working\" in yellow and other messages will be displayed in red.
    It is recommended to use short, single sentence.
    RoboDK may take action based on the status message, such as stopping subsequent commands.
    """
    global LOCK_STDOUT
    with LOCK_STDOUT:
        print("SMS:" + status)
        sys.stdout.flush()


def show_message(message):
    """
    Display a message in the status bar of the main window of RoboDK (SMS2:).
    """
    global LOCK_STDOUT
    with LOCK_STDOUT:
        print("SMS2:" + message)
        sys.stdout.flush()


def print_message(message):
    """
    Display a informative/debug message in the connection log window of RoboDK (DBG:).
    RoboDK will not take action based on the debug message.
    """
    global LOCK_STDOUT
    with LOCK_STDOUT:
        print("DBG:" + message)
        sys.stdout.flush()


def print_response(response):
    """
    Report the driver status/response for API Driver command (RE:).

    i.e. \"resp = robot.setParam('Driver', some_command)\".
    """
    global LOCK_STDOUT
    with LOCK_STDOUT:
        print("RE:" + response)
        sys.stdout.flush()


def print_commands(commands):
    """
    Display a list of available custom commands (c) in the connection log window of RoboDK (CMDLIST:).
    RoboDK will add a menu to the connection widget to trigger these from the GUI.

    e.g. \"c Command1|This is a command|c Command2|This is a second command|\"
    """
    global LOCK_STDOUT
    with LOCK_STDOUT:
        print("CMDLIST:" + commands)
        sys.stdout.flush()


def print_joints(joints, is_moving=False):
    """
    Report the robot joints to RoboDK (JNTS: or JNTS_MOVING:).
    Provide accurate joint values when the robot is static (JNTS:), or less precision if the robot is currently moving (JNTS_MOVING:).
    """
    global LOCK_STDOUT
    with LOCK_STDOUT:
        if is_moving:
            print("JNTS_MOVING: " + " ".join(format(x, ".3f") for x in joints))
        else:
            print("JNTS: " + " ".join(format(x, ".6f") for x in joints))
        sys.stdout.flush()



try:
    import frrpc
except:
    print_status("frrpc is not installed, please install it.") #To be prettied up
#-----------------------------------------------------------------------------
# Pre-defined statuses to synchronize RoboDK driver state

ROBOTCOM_UNKNOWN = -1000
ROBOTCOM_CONNECTION_PROBLEMS = -3
ROBOTCOM_DISCONNECTED = -2
ROBOTCOM_NOT_CONNECTED = -1
ROBOTCOM_READY = 0
ROBOTCOM_WORKING = 1
ROBOTCOM_WAITING = 2

global STATUS
STATUS = ROBOTCOM_DISCONNECTED


def UpdateStatus(set_status=None):
    """
    UpdateStatus will send an appropriate message to RoboDK which will result in a specific coloring and actions.
    For example, \"Ready\" will be displayed in green, \"Waiting\" in blue, \"Working\" in yellow
    and other messages will be displayed in red.

    e.g. SMS:Ready
    """
    global STATUS
    if set_status is not None:
        STATUS = set_status

    if STATUS == ROBOTCOM_CONNECTION_PROBLEMS:
        print_status("Connection problems")
    elif STATUS == ROBOTCOM_DISCONNECTED:
        print_status("Disconnected")
    elif STATUS == ROBOTCOM_NOT_CONNECTED:
        print_status("Not connected")
    elif STATUS == ROBOTCOM_READY:
        print_status("Ready")
    elif STATUS == ROBOTCOM_WORKING:
        print_status("Working...")
    elif STATUS == ROBOTCOM_WAITING:
        print_status("Waiting...")
    else:
        print_status("Unknown status")


#-----------------------------------------------------------------------------
# RoboDK known commands, mostly as a reference

RDK_COMMANDS = {
    'CONNECT',  # Connect request. RoboDK provides IP, port and DOF. i.e. CONNECT 192.168.0.100 10000 6
    'DISCONNECT',  # Disconnect request. i.e. DISCONNECT
    'STOP',  # Stop the driver (process terminate). i.e. STOP 192.168.0.100
    'QUIT',  # Stop the driver (process terminate). i.e. QUIT 192.168.0.100
    'PAUSE',  # Run a pause. RoboDK provides time in ms. i.e. PAUSE 500
    'MOVJ',  # Execute a joint move. RoboDK provides j1,j2,...,jN,j7,x,y,z,w,p,r. i.e. MOVJ -36.346835 30.257813 -15.386742 0.000000 75.128930 -36.346835 156.061707 -114.835243 187.538361 -180.000000 0.000000 180.000000
    'MOVL',  # Execute a linear move. RoboDK provides j1,j2,...,jN,j7,x,y,z,w,p,r. i.e. MOVL 18.676422 16.809842 3.809454 0.000000 69.380704 18.676422 156.061707 52.752403 187.538361 -180.000000 0.000000 180.000000
    'MOVLSEARCH',  # Execute a linear search move. RoboDK provides j1,j2,...,jN,x,y,z,w,p,r.i.e. MOVLSEARCH 18.676422 16.809842 3.809454 0.000000 69.380704 18.676422 156.061707 52.752403 187.538361 -180.000000 0.000000 180.000000
    'MOVC',  # Execute a circular move. RoboDK provides t1j1,t1j2,...,t1jN,t2j1,t2j2,...,t2jN,t1x,t1y,t1z,t1w,t1p,t1r,t2x,t2y,t2z,t2w,t2p,t2r. i.e. MOVC -4.35157 43.058 -36.4316 2.461e-14 83.3736 -4.35157 -36.3468 30.2578 -15.3867 2.46089e-14 75.1289 -36.3468 215.131 -16.3705 187.538 -180 -3.70949e-15 180 156.062 -114.835 187.538 -180 3.07043e-14 180
    'CJNT',  # Request to retrieve the current joint position of the robot. i.e. CJNT
    'SPEED',  # Set the speeds. RoboDK provides mm/s,deg/s,m/s2,deg/s2. i.e. SPEED 1.000 2.000 3.000 4.000
    'SETROUNDING',  # Set the rounding/smoothing value. Also known as ZoneData in ABB or CNT for Fanuc. i.e. SETROUNDING -1
    'SETDO',  # Set a digital output on the controller. IO_NAME, IO_VALUE i.e. SETDO 5 1 5 1, or SETDO 0 0 VAR VALUE
    'SETAO',  # Set an analog output on the controller. IO_NAME, IO_VALUE i.e. SETAO 5 1 5 1, or SETDA 0 0 VAR VALUE
    'WAITDI',  # Wait for a digital input on the controller. IO_NAME, IO_VALUE i.e. WAITDI 5 1 5 1, or WAITDI 0 0 VAR VALUE
    'SETTOOL',  # Set the Tool reference frame provided the 6 XYZWPR cmd_values by RoboDK. i.e. SETTOOL 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
    'RUNPROG',  # Run a program call on the controller. i.e.  RUNPROG 10 Program_10 or RUNPROG -1 ProgramCall
    'POPUP',  # Display a pop-up message on the teach-pendant (if applicable). i.e. POPUP This is a message
    'SUPPORTSEARCH',  # TODO
    'SEARCHDI',  # TODO
}

DEBUG_COMMANDS = [
    't',  # Test the driver shortcut
    'c',  # Connect shortcut using presets
    'd',  # Disconnect shortcut
    's',  # Stop shortcut
    'q',  # Quit shortcut
]

DRIVER_RESPONSES = {
    'SMS': None,  # Will display a message in the log window and the connection status bar
    'SMS2': None,  # Will display a message in the status bar of the main window
    'DBG': None,
    'RE': None,  # Used to retrieve statues through the API using robot.setParam('Driver', command)
    'CMDLIST': None,  # Used to provide additional commands to the user in the log window
}

#-----------------------------------------------------------------------------
# Console commands management


def str_to_float(str_value):
    """Converts a string to it's numerical counterpart. Returns the original string if it is not an int/float"""
    try:
        if '.' in str_value:
            return float(str_value)
        else:
            return int(str_value)
    except ValueError:
        return str_value


def command_parser(cmd_line):
    """Parse a string-command from the STDIN"""
    if not cmd_line.strip():
        return '', []

    args = [arg.strip() for arg in cmd_line.split(' ')]
    if not args:
        return '', []

    cmd = args[0]
    args = [str_to_float(arg) for arg in args[1:]]
    return cmd, args


def command_runner(cmd, args, robot: 'RobotDriver'):
    """
    Parse a RoboDK command string (STDIN or command line) and forward the appropriate action to the robot communication class.

    Command strings are prefixed with a command, followed by its arguments (space-separated).
    Joint values are in degrees, and follows the number of axis of the robot.
    Poses use the XYZRPW format (in millimeters/degrees).

    :param str cmd_line: The command string

    Available commands:

    .. code-block:: text

        CONNECT       Connect request. RoboDK provides IP (or COM port), port and DOF.
                      i.e. CONNECT 192.168.0.100 10000 6
                      i.e. CONNECT COM4 10000 6

        DISCONNECT    Disconnect request.
                      i.e. DISCONNECT

        QUIT          Stop the driver (process terminate).
                      i.e. QUIT

        STOP          Stop the motion, if supported.
                      i.e. STOP 192.168.0.100

        PAUSE         Run a pause. RoboDK provides time in ms.
                      i.e. PAUSE 500

        MOVJ          Execute a joint move. RoboDK provides joints and pose.
                      The number of joints depends on the degrees of freedom of the robot (including synchronized external axis).
                      RoboDK provides the pose with the same euler convention found in robot->Parameters.
                      i.e. MOVJ -36.3 30.2 -15.3 0.0 75.1 -36.3 156.0 -114.8 187.5 -180.0 0.0 180.0

        MOVL          Execute a linear move. RoboDK provides joints and pose.
                      The number of joints depends on the degrees of freedom of the robot (including synchronized external axis).
                      RoboDK provides the pose with the same euler convention found in robot->Parameters.
                      i.e. MOVL 18.6 16.8 3.8 0.0 69.3 18.6 156.0 52.7 187.5 -180.0 0.0 180.0

        MOVLSEARCH    Execute a linear search move. RoboDK provides joints and pose.
                      The number of joints depends on the degrees of freedom of the robot (including synchronized external axis).
                      RoboDK provides the pose with the same euler convention found in robot->Parameters.
                      i.e. MOVLSEARCH 18.6 16.8 3.8 0.0 69.3 18.6 156.0 52.7 187.5 -180.0 0.0 180.0

        MOVC          Execute a circular move. RoboDK provides joints 1 (mid point), joints 2 (end point), pose 1 (mid point) and pose 2 (end point).
                      The number of joints depends on the degrees of freedom of the robot (including synchronized external axis).
                      RoboDK provides the pose with the same euler convention found in robot->Parameters.
                      i.e. MOVC -4.3 43.0 -36.4 0.0 83.3 -4.3 -36.3 30.2 -15.3 0.0 75.1 -36.3 215.1 -16.3 187.5 -180 0.0 180 156.0 -114.8 187.5 -180 0.0 180

        CJNT          Request to retrieve the current joint position of the robot.
                      i.e. CJNT

        SPEED         Set the speeds. RoboDK provides mm/s, deg/s, m/s2 and deg/s2.
                      A value below zero means that the sped should remain the same.
                      i.e. SPEED 1000.0 200.0 1500.000 150.000
                      i.e. SPEED 1000.0 -1 -1 -1

        SETROUNDING   Set the rounding/smoothing/blending value in mm. Sub-zero values means accurate.
                      i.e. SETROUNDING -1
                      i.e. SETROUNDING 10

        SETDO         Set a digital output on the controller. RoboDK provides the IO name, IO value, variable name and variable value.
                      i.e. SETDO 5 1 5 1
                      i.e. SETDO 0 0 VARNAME VALUE

        SETAO         Set a analog output on the controller. RoboDK provides the IO name, IO value, variable name and variable value.
                      i.e. SETAO 5 1 5 1
                      i.e. SETAO 0 0 VARNAME VALUE

        WAITDI        Wait for a digital input on the controller. RoboDK provides the IO name, IO value, variable name and variable value.
                      i.e. WAITDI 5 1 5 1
                      i.e. WAITDI 0 0 VAR VALUE

        SETTOOL       Set the Tool Center Point.
                      RoboDK provides the pose with the same euler convention found in robot->Parameters.
                      i.e. SETTOOL 0.0 0.0 0.0 0.0 0.0 0.0

        RUNPROG       Run a program call on the controller. RoboDK provides the program ID (if any) and the program name.
                      i.e.  RUNPROG 10 Program_10 or RUNPROG -1 ProgramCall

        POPUP         Display a pop-up message on the teach-pendant (if applicable). RoboDK provides the message.
                      i.e. POPUP This is a message

    """
    if cmd == 'c' and args:
        # Forward commands directly as is. ie 'c SendThis(1, 2, 3)'
        robot.RunCode(' '.join(args))
        return

    if cmd not in RDK_COMMANDS:
        return

    if cmd not in ['CONNECT', 'DISCONNECT', 'QUIT'] and not robot.Connected():
        UpdateStatus(ROBOTCOM_NOT_CONNECTED)
        return

    #------------------------------------------------
    if cmd == 'CONNECT':
        UpdateStatus(ROBOTCOM_WAITING)

        success = False
        if not args:
            success = robot.ConnectDefault()
        else:
            ip_or_com = args[0]  # IP or COM port
            port = args[1]
            ndof = args[2]
            success = robot.Connect(ip_or_com, port, ndof)

        if not success:
            UpdateStatus(ROBOTCOM_CONNECTION_PROBLEMS)
        else:
            joints = robot.Joints()
            print_joints(joints)

            UpdateStatus(ROBOTCOM_READY)

    #------------------------------------------------
    elif cmd == 'DISCONNECT':
        robot.Disconnect()
        UpdateStatus(ROBOTCOM_DISCONNECTED)

    #------------------------------------------------
    elif cmd in ['QUIT']:
        # NOTE: In some drivers, both STOP and QUIT were use to kill the process
        UpdateStatus(ROBOTCOM_WORKING)

        robot.Stop()
        robot.Disconnect()

        UpdateStatus(ROBOTCOM_DISCONNECTED)
        #sys.exit(0)

    #------------------------------------------------
    elif cmd in ['STOP']:
        # NOTE: In some drivers, both STOP and QUIT were use to kill the process
        UpdateStatus(ROBOTCOM_WORKING)

        robot.Stop()

        UpdateStatus(ROBOTCOM_READY)

    #------------------------------------------------
    elif cmd in ["SUPPORTSEARCH"]:
        #UpdateStatus(ROBOTCOM_WORKING)

        print_response("1" if robot.hasLinearSearch() else "0")

        UpdateStatus(ROBOTCOM_READY)

    #------------------------------------------------
    elif cmd in ["SEARCHDI"]:
        #UpdateStatus(ROBOTCOM_WORKING)

        #var_name, var_value = args[:2]

        #robot.setSearchCondition(var_name, var_value)
        print_message("SEARCHDI cmd not implemented yet")
        UpdateStatus(ROBOTCOM_READY)

    #------------------------------------------------
    elif cmd in ['MOVLSEARCH']:
        UpdateStatus(ROBOTCOM_WORKING)

        #ndof = len(args) - 6
        #joints = args[:ndof]
        #xyzwpr = args[-6:]

        #success, contact_joints = robot.SearchL(xyzwpr, joints)
        #print_response("1" if success else "0")

        # joints = robot.Joints()
        #print_joints(joints)
        #print_joints(contact_joints)
        print_message("MOVLSEARCH cmd not implemented yet")
            

        UpdateStatus(ROBOTCOM_READY)

    #------------------------------------------------
    elif cmd in ['MOVJ', 'MOVL']:
        UpdateStatus(ROBOTCOM_WORKING)

        ndof = len(args) - 6
        joints = args[:ndof]
        xyzwpr = args[-6:]

        if cmd == 'MOVJ':
            robot.MoveJ(xyzwpr, joints)
        elif cmd == 'MOVL':
            robot.MoveJ(xyzwpr, joints)

        joints = robot.Joints()
        print_joints(joints)

        UpdateStatus(ROBOTCOM_READY)

    #------------------------------------------------
    elif cmd in ['MOVC']:
        UpdateStatus(ROBOTCOM_WORKING)

        #ndof = (len(args) - 12) / 2
        #joints1 = args[:ndof]
        #joints2 = args[ndof:2 * ndof]
        #xyzwpr1 = args[-12:-6]
        #xyzwpr2 = args[-6:]

        #robot.MoveC(xyzwpr1, joints1, xyzwpr2, joints2)

        #joints = robot.Joints()
        #print_joints(joints)
        print_message("MOVC cmd not implemented yet")
        UpdateStatus(ROBOTCOM_READY)

    #------------------------------------------------
    elif cmd in ['SPEED']:
        #UpdateStatus(ROBOTCOM_WORKING)

        lin_vel, joint_vel, lin_accel, joint_accel = args[:4]
        if lin_vel >= 0:
            robot.setSpeed(lin_vel)
        if joint_vel >= 0:
            #robot.setSpeedJoints(joint_vel)
            print_message("SPEEDJOINTS cmd not implemented yet")
            UpdateStatus(ROBOTCOM_READY)
        if lin_accel >= 0:
            robot.setAcceleration(lin_accel)
            
            UpdateStatus(ROBOTCOM_READY)
        if joint_accel >= 0:
            #robot.setAccelerationJoints(joint_accel)
            print_message("ACCJOINTS cmd not implemented yet")
            UpdateStatus(ROBOTCOM_READY)

        UpdateStatus(ROBOTCOM_READY)

    #------------------------------------------------
    elif cmd in ['CJNT']:
        #UpdateStatus(ROBOTCOM_WORKING)

        joints = robot.Joints()
        print_joints(joints)

        UpdateStatus(ROBOTCOM_READY)

    #------------------------------------------------
    elif cmd in ['PAUSE']:
        UpdateStatus(ROBOTCOM_WORKING)

        time_ms = args[0]
        robot.Pause(time_ms)

        UpdateStatus(ROBOTCOM_READY)

    #------------------------------------------------
    elif cmd in ['SETROUNDING']:
        #UpdateStatus(ROBOTCOM_WORKING)

        print_message("SETROUNDING cmd not implemented yet")
        UpdateStatus(ROBOTCOM_READY)


    #------------------------------------------------
    elif cmd in ['SETDO']:
        #UpdateStatus(ROBOTCOM_WORKING)

        do_name, do_value, var_name, var_value = args[:4]
        robot.setDO(var_name, var_value)

        UpdateStatus(ROBOTCOM_READY)

    #------------------------------------------------
    elif cmd in ['WAITDI']:
        #UpdateStatus(ROBOTCOM_WORKING)

        di_name, di_value, var_name, var_value = args[:4]
        robot.waitDI(var_name, var_value)

        UpdateStatus(ROBOTCOM_READY)

    #------------------------------------------------
    elif cmd in ['SETTOOL']:
        #UpdateStatus(ROBOTCOM_WORKING)

        #xyzwpr = args[:6]
        #robot.setTool(xyzwpr)

        print_message("SETTOOL cmd not implemented yet")
        UpdateStatus(ROBOTCOM_READY)

    #------------------------------------------------
    elif cmd in ['RUNPROG']:
        UpdateStatus(ROBOTCOM_WORKING)

        #prog_id, prog_name = args[:2]
        #robot.RunCode(prog_name)
        print_message("RUNPROG cmd not implemented yet")
            

        UpdateStatus(ROBOTCOM_READY)

    #------------------------------------------------
    elif cmd in ['POPUP']:
        UpdateStatus(ROBOTCOM_WORKING)

        #message = ' '.join(args)
        #robot.RunMessage(message)
        print_message("POPUP cmd not implemented yet")
        UpdateStatus(ROBOTCOM_READY)



class DriverManager:

    def __init__(self, robot):
        self.commands_queue = Queue()
        self.thread_driver = None
        self.thread_console = None
        self.robot = robot

    def ConsoleRunner(self):
        for line in sys.stdin:
            cmd, args = command_parser(line)
            if not cmd:
                continue

            if cmd in ['QUIT']:
                with self.commands_queue.mutex:
                    self.commands_queue.queue.clear()

            self.commands_queue.put((cmd, args))

            if cmd in ['QUIT']:
                return

    def DriverRunner(self):
        while True:

            if self.commands_queue.empty():
                time.sleep(0.005)
                continue

            cmd, args = self.commands_queue.get()
            if cmd == 't':
                self.TestDriver()
                continue

            # Developer shortcuts
            if cmd == 'c' and not args:
                cmd = 'CONNECT'
            elif cmd == 'd' and not args:
                cmd = 'DISCONNECT'
            elif cmd == 's' and not args:
                cmd = 'STOP'
            elif cmd == 'q' and not args:
                cmd = 'QUIT'

            command_runner(cmd, args, self.robot)

            if cmd in ['QUIT']:
                return

    def Run(self):
        if self.thread_driver is not None:
            return

        print_commands(self.robot.CustomCommandList())

        self.thread_console = threading.Thread(target=self.ConsoleRunner, name="ConsoleRunner", args=())
        self.thread_console.daemon = True
        self.thread_console.start()

        # Run the driver in the main thread
        if True:
            self.DriverRunner()
        else:
            self.thread_driver = threading.Thread(target=self.DriverRunner, name="DriverRunner", args=())
            self.thread_driver.daemon = True
            self.thread_driver.start()

    def Stop(self):
        self.commands_queue.put('QUIT', [])
        if self.thread_console is not None and self.thread_console.is_alive():
            self.thread_console.join(5)
            self.thread_console = None

        if self.thread_driver is not None and self.thread_driver.is_alive():
            self.thread_driver.join(5)
            self.thread_driver = None

    def TestDriver(self):
        # Sample set of commands that can be provided by RoboDK of through the command line

        # TEMPLATE/TODO: This presets are meant for a 6 axis robot. Update them accordingly!
        commands = [
            "CONNECT",
            "SPEED 10 -1 -1 -1",
            "SETTOOL -0.025 -41.046 50.920 60.000 -0.000 90.000",
            "CJNT",
            "MOVJ 0 -5.362010 46.323420 20.746290 74.878840 -50.101680 61.958500",
            "MOVJ 500 -5.362010 46.323420 20.746290 74.878840 -50.101680 61.958500",
            "SPEED 100 -1 -1 -1",
            "MOVL 0 -5.362010 50.323420 20.746290 74.878840 -50.101680 61.958500",
            "MOVL 500 -5.362010 50.323420 20.746290 74.878840 -50.101680 61.958500",
            "PAUSE 1000",
            "DISCONNECT",
        ]

        for line in commands:
            cmd, args = command_parser(line)
            if not cmd:
                continue
            self.commands_queue.put((cmd, args))


# ----------------------------------------------------
# Object class that handles the robot instructions/syntax
# TEMPLATE/TODO: This is the class that must be updated for your specific device
class RobotDriver(object):
    """Robot Driver object"""

    def __init__(self):

        # Driver specific
        self.robot = None
        self.vel = 20.0
        self.acc = 0.0
        # Keep after member initialization
        self._manager = DriverManager(self)

    def DriverRun(self):
        self._manager.Run()
        return self

    def DriverKill(self):
        self.Disconnect()
        self._manager.Stop()

    def __del__(self):
        self.DriverKill()

    def CustomCommandList(self):
        """
        Returns a list of custom/additional commands that can be sent as-is, directly to the device through the communication stream.
        This is to add functionalities to the driver that are not built into RoboDK, such as home the robot, reset errors, initialization routines, etc.
        See CMDLIST.
        """
        # Example:
        # commands = {
        #     'RESET': "Reset errors",
        #     'HOME': "Home the robot",
        # }

        commands = {}

        return "|".join([f'c {cmd}|{info}' for cmd, info in commands.items()])

    def Connect(self, ip_or_com: str, port, dof, *args, **kwargs):
        """
        Request to connect to the robot. RoboDK provides IP or COM, port and degrees of freedom (DOF).

        i.e. TCP: CONNECT 192.168.0.100 10000 1
        i.e. COM: CONNECT COM3 -1 1
        """
        self.robot = frrpc.RPC(ip_or_com)
        return True

    def ConnectDefault(self, *args, **kwargs):
        """
        Helper function to quickly connect the driver using default/typical connection settings.
        """
        self.robot = frrpc.RPC()
        return True

    def Connected(self, force_check=False, *args, **kwargs):
        """
        Request connection status.
        """
        return self.robot.sock_cli_state_state

    def Disconnect(self, *args, **kwargs):
        """
        Request to disconnect from the robot.

        i.e. DISCONNECT
        """
        self.robot.CloseRPC()
        
    def Stop(self, *args, **kwargs):
        """
        Request to stop the robot and clear any upcoming motion commands.

        i.e. STOP 192.168.0.100
        """
        self.robot.StopMotion()

    def Joints(self, *args, **kwargs):
        """
        Return the current joint position as a :class:`~robodk.robomath.Mat` of a robot or the joints of a target.

        Request to retrieve the current joint position of the robot.
        i.e. CJNT
        """
        return self.robot.GetActualJointPosDegree(0)[1]

    def MoveJ(self, pose, joints, conf_RLF=None, *args, **kwargs):
        """
        Defines a joint movement.

        **Tip**:
        MoveJ is triggered by the RoboDK instruction Program->Move Joint Instruction.

        :param pose: Pose target of the tool with respect to the reference frame. Pose can be None if the target is defined as a joint target
        :type pose: :meth:`robodk.robomath.Mat`, None
        :param joints: Robot joints as a list
        :type joints: float list
        :param conf_RLF: Robot configuration as a list of 3 ints: [REAR, LOWER-ARM, FLIP]. [0,0,0] means [front, upper arm and non-flip] configuration. Configuration can be None if the target is defined as a joint target
        :type conf_RLF: int list, None

        Execute a joint move. RoboDK provides joints and pose.
        i.e. MOVJ -36.3 30.2 -15.3 0.0 75.1 -36.3 156.0 -114.8 187.5 -180.0 0.0 180.0
        """
        self.robot.MoveJ(joints, 0, 0, vel=self.vel, acc=self.acc)

    def MoveL(self, pose, joints, conf_RLF=None, *args, **kwargs):
        """Defines a linear movement.

        **Tip**:
        MoveL is triggered by the RoboDK instruction Program->Move Linear Instruction.

        :param pose: Pose target of the tool with respect to the reference frame. Pose can be None if the target is defined as a joint target
        :type pose: :meth:`robodk.robomath.Mat`, None
        :param joints: Robot joints as a list
        :type joints: float list
        :param conf_RLF: Robot configuration as a list of 3 ints: [REAR, LOWER-ARM, FLIP]. [0,0,0] means [front, upper arm and non-flip] configuration. Configuration can be None if the target is defined as a joint target
        :type conf_RLF: int list, None
        """
        self.robot.MoveL(pose, 0, 0, vel=self.vel, acc=self.acc)

    def MoveC(self,
              pose1,
              joints1,
              pose2,
              joints2,
              conf_RLF_1=None,
              conf_RLF_2=None,
              *args,
              **kwargs):
        """Defines a circular movement.

        **Tip**:
        MoveC is triggered by the RoboDK instruction Program->Move Circular Instruction.

        :param pose1: Pose target of a point defining an arc (waypoint). Pose can be None if the target is defined as a joint target
        :type pose1: :meth:`robodk.robomath.Mat`, None
        :param pose2: Pose target of the end of the arc (final point). Pose can be None if the target is defined as a joint target
        :type pose2: :meth:`robodk.robomath.Mat`, None
        :param joints1: Robot joints of the waypoint
        :type joints1: float list
        :param joints2: Robot joints of the final point
        :type joints2: float list
        :param conf_RLF_1: Robot configuration of the waypoint as a list of 3 ints: [REAR, LOWER-ARM, FLIP]. [0,0,0] means [front, upper arm and non-flip] configuration. Configuration can be None if the target is defined as a joint target
        :type conf_RLF_1: int list, None
        :param conf_RLF_2: Robot configuration of the final point as a list of 3 ints: [REAR, LOWER-ARM, FLIP]. [0,0,0] means [front, upper arm and non-flip] configuration. Configuration can be None if the target is defined as a joint target
        :type conf_RLF_2: int list, None
        """
        raise

    def SearchL(self, joints, xyzwpr, *args, **kwargs):
        # TODO
        raise

    def hasLinearSearch(self):
        # TODO
        return 0

    def setTool(self, pose, tool_id=None, tool_name=None, *args, **kwargs):
        """Change the robot TCP (Tool Center Point) with respect to the robot flange. Any movement defined in Cartesian coordinates assumes that it is using the last reference frame and tool frame provided.

        **Tip**:
        setTool is triggered by the RoboDK instruction Program->Set Tool Frame Instruction.

        :param pose: Pose of the TCP frame with respect to the robot base frame
        :type pose: :meth:`robodk.robomath.Mat`
        :param tool_id: Tool ID (if available, else -1)
        :type tool_id: int, None
        :param tool_name: Name of the tool as defined in RoboDK
        :type tool_name: str
        """
        #print(pose)
        #self.robot.SetToolCoord(1,pose, 1, 0, 1, 1)
        pass

    def Pause(self, time_ms, *args, **kwargs):
        """Defines a pause in a program (including movements). time_ms is negative if the pause must provoke the robot to stop until the user desires to continue the program.

        **Tip**:
        Pause is triggered by the RoboDK instruction Program->Pause Instruction.

        :param time_ms: Time of the pause, in milliseconds
        :type time_ms: float
        """
        self.robot.WaitMs(time_ms)

    def setSpeed(self, speed_mms, *args, **kwargs):
        """Changes the robot speed (in mm/s)

        **Tip**:
        setSpeed is triggered by the RoboDK instruction Program->Set Speed Instruction.

        :param speed_mms: Speed in :math:`mm/s`
        :type speed_mms: float
        """
        #Converts typical speed of 1m/s to ratio with a max of 100%
        self.vel = min(speed_mms, 100.0)
        self.robot.SetSpeed(self.vel)


    def setAcceleration(self, accel_mmss, *args, **kwargs):
        """Changes the robot acceleration (in mm/s^2)

        **Tip**:
        setAcceleration is triggered by the RoboDK instruction Program->Set Speed Instruction. An acceleration value must be provided.

        :param accel_mmss: Speed in :math:`mm/s^2`
        :type accel_mmss: float
        """
        self.acc = min(accel_mmss, 100.0)


    def setSpeedJoints(self, speed_degs, *args, **kwargs):
        """Changes the robot joint speed (in deg/s)

        **Tip**:
        setSpeedJoints is triggered by the RoboDK instruction Program->Set Speed Instruction. A joint speed value must be provided.

        :param speed_degs: Speed in :math:`deg/s`
        :type speed_degs: float
        """
        raise

    def setAccelerationJoints(self, accel_degss, *args, **kwargs):
        """Changes the robot joint acceleration (in deg/s^2)

        **Tip**:
        setAccelerationJoints is triggered by the RoboDK instruction Program->Set Speed Instruction. A joint acceleration value must be provided.

        :param accel_degss: Speed in :math:`deg/s^2`
        :type accel_degss: float
        """
        raise

    def setZoneData(self, zone_mm, *args, **kwargs):
        """Changes the smoothing radius (also known as rounding, blending radius, CNT, APO or zone data). If this parameter is higher it helps making the movement smoother

        **Tip**:
        setZoneData is triggered by the RoboDK instruction Program->Set Rounding Instruction.

        :param zone_mm: Rounding radius in mm. A value of 0 or less means disable
        :type zone_mm: float
        """
        raise

    def setDO(self, io_var, io_value, *args, **kwargs):
        """Sets a variable (usually a digital output) to a given value. This method can also be used to set other variables.

        **Tip**:
        setDO is triggered by the RoboDK instruction Program->Set or Wait I/O Instruction.

        :param io_var: Variable to set, provided as a str or int
        :type io_var: int, str
        :param io_value: Value of the variable, provided as a str, float or int
        :type io_value: int, float, str
        """
        self.robot.SetDO(io_var, io_value)

    def setAO(self, io_var, io_value):
        """Sets a an analog variable to a given value.

        **Tip**:
        setAO is triggered by the RoboDK instruction Program->Set or Wait I/O Instruction.

        :param io_var: Variable to set, provided as a str or int
        :type io_var: int, str
        :param io_value: Value of the variable, provided as a str, float or int
        :type io_value: int, float, str
        """
        self.robot.SetAO(io_var, io_value)

    def waitDI(self, io_var, io_value, timeout_ms=-1, *args, **kwargs):
        """Waits for a variable (usually a digital input) to attain a given value io_value. This method can also be used to set other variables.Optionally, a timeout can be provided.

        **Tip**:
        waitDI is triggered by the RoboDK instruction Program->Set or Wait I/O Instruction.

        :param io_var: Variable to wait for, provided as a str or int
        :type io_var: int, str
        :param io_value: Value of the variable, provided as a str, float or int
        :type io_value: int, float, str
        :param timeout_ms: Maximum wait time
        :type timeout_ms: float, int
        """
        self.robot.WaitDI(io_var, io_value, timeout_ms, 2)

    def RunCode(self, code, is_function_call=False, *args, **kwargs):
        """Adds code or a function call.

        **Tip**:
        RunCode is triggered by the RoboDK instruction Program->Function call Instruction.

        :param code: Code or procedure to call
        :param is_function_call: True if the provided code is a specific function to call
        :type code: str
        :type is_function_call: bool
        """
        raise

    def RunMessage(self, message, iscomment=False, *args, **kwargs):
        """Display a message in the robot controller screen (teach pendant)

        **Tip**:
        RunMessage is triggered by the RoboDK instruction Program->Show Message Instruction.

        :param message: Message to display on the teach pendant or as a comment on the code
        :type message: str
        :param iscomment: True if the message does not have to be displayed on the teach pendant but as a comment on the code
        :type iscomment: bool
        """
        raise


def RunMain():

    print_message(DRIVER_VERSION)

    UpdateStatus(ROBOTCOM_DISCONNECTED)

    driver = RobotDriver().DriverRun()
    driver.DriverKill()

    print_message("Process terminated cleanly")


if __name__ == "__main__":
    RunMain()

