#*****************************************************************************
#
#   Module:         main.py
#   Project:        UC-02-2024 Coffee Cart Modifications
#
#   Repository:     robot_barista
#   Target:         N/A
#
#   Author:         Rodney Elliott
#   Date:           23 June 2025
#
#   Description:    Make or fake the making of a double espresso using the 2025
#                   iteration of the ENMT482 (Robotics) laboratory apparatus.
#
#*****************************************************************************
#
#   Copyright:      (C) 2025 Rodney Elliott
#
#   This file is part of Coffee Cart Modifications.
#
#   Coffee Cart Modifications is free software: you can redistribute it and/or
#   modify it under the terms of the GNU General Public License as published by
#   the Free Software Foundation, either version 3 of the license, or (at your
#   option) any later version.
#
#   Coffee Cart Modifications is distributed in the hope that it will be useful,
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
#   Public License for more details.
#
#   You should of received a copy of the GNU General Public License along with
#   Coffee Cart Modifications. If not, see <https://www.gnu.org/licenses/>.
#
#*****************************************************************************

#*****************************************************************************
#   Doxygen file documentation
#*****************************************************************************

##
#   @file main.py
#
#   @brief ENMT482 manipulator assignment demonstration code.
#
#   _robot_barista_ makes or fakes the making of a double espresso using the
#   2025 iteration of the ENMT482 (Robotics) laboratory apparatus. The code is
#   robot / coffee cart agnostic, meaning that it may be run on any one of the
#   carts without requiring modification. Furthermore, the code is considered
#   human-safe, and as a result may be used for publicity events as well as
#   in-class demonstrations.

#*****************************************************************************
#   Modules
#*****************************************************************************

##
#   @package sys
#
#   The _sys_ module provides access to variables and functions that interact
#   with the Python interpreter and the operating system.
import sys

##
#   @package argparse
#
#   The _argparse_ module makes it easy to write user-friendly command-line
#   interfaces.
import argparse

##
#   @package numpy
#
#   _numpy_ is the fundamental package for scientific computing with Python.
import numpy as np
import numpy.linalg as la

##
#   @package pandas
#
#   _pandas_ is a fast, powerful, flexible and easy to use open source data
#   analysis and manipulation tool built on top of the Python programming
#   language.
import pandas as pd

##
#   @package openpyxl
#
#   _openpyxl_ is a Python library to read/write Excel 2010 xlsx/xlsm/xltx/xltm
#   files.
import openpyxl

##
#   @package robodk.robolink
#
#   The robodk package is the distributed entry point of the Python API. It is
#   the common parent of all sub-packages and modules that constitute the
#   Python API.
#
#   The _robolink_ sub-module is the bridge between RoboDK and Python. Every
#   object in the RoboDK item tree can be retrieved and it is represented by
#   the object Item. An item can be a robot, a reference frame, a tool, an
#   object or any other item visible in the station tree.
from robodk.robolink import *

##
#   @package robodk.robodialogs
#
#   The robodk package is the distributed entry point of the Python API. It is
#   the common parent of all sub-packages and modules that constitute the
#   Python API.
#
#   The _robodialogs_ sub-module is a dialogs toolbox. It contains, open and
#   save file dialogs, message prompts, input dialogs etc.
import robodk.robodialogs as rd

##
#   @package robodk.robomath
#
#   The robodk package is the distributed entry point of the Python API. It is
#   the common parent of all sub-packages and modules that constitute the
#   Python API.
#
#   The _robomath_ module is a robotics toolbox for Python, based on Peter
#   Corke’s Robotics Toolbox (regarding pose transformations):
#   https://petercorke.com/toolboxes/robotics-toolbox/. The robomath sub-module
#   includes the Mat class to represent transformations in 3D.
from robodk.robomath import *

##
#   @package modbus_scale_client
#
#   The _modbus_scale_client_ package contains the Modbus Ethernet client
#   class.
from modbus_scale_client import modbus_scale_client

##
#   @package common
#
#   The _common_ module contains configuration information common to other
#   project modules.
import common

##
#   @package points
#
#   The _points_ module contains methods to retrieve data from the Excel points
#   spreadsheet.
import points

##
#   @package frames
#
#   The _frames_ module contains methods to compute and display frame HT
#   matrices.
import frames

##
#   @package targets
#
#   The _targets_ module contains methods to compute and display target HT
#   matrices.
import targets

##
#   @package tasks
#
#   The _tasks_ module contains methods to perform ENMT482 manipulator
#   assignment tasks.
import tasks

#*****************************************************************************
#   Variables
#*****************************************************************************

##
#   Scale IPv4 addresses.
#
#   Note that each robot / coffee cart has its own private `/24` subnet, and
#   the only laboratory computers able to access these networks are the ones
#   connected via the 8-port switch located on each cart backplane.
scale_address = {
    "IP_MAZZER_1": "192.168.20.3",
    "IP_MAZZER_2": "192.168.21.3",
    "IP_MAZZER_3": "192.168.22.3",
    "IP_RANCILIO_1": "192.168.20.4",
    "IP_RANCILIO_2": "192.168.21.4",
    "IP_RANCILIO_3": "192.168.22.4"
}

## 
#   RoboDK mode.
#
#   RoboDK supports a number of run modes.
#
#   | Name                              | Value | Description                                                                          |
#   | --------------------------------- | ----- |------------------------------------------------------------------------------------- |
#   | RUNMODE_SIMULATE                  | 1     | Performs the simulation moving the robot (default)                                   |
#   | RUNMODE_QUICKVALIDATE             | 2     | Performs a quick check to validate the robot movements                               |
#   | RUNMODE_MAKE_ROBOTPROG            | 3     | Makes the robot program                                                              |
#   | RUNMODE_MAKE_ROBOTPROG_AND_UPLOAD | 4     | Makes the robot program and updates it to the robot                                  |
#   | RUNMODE_MAKE_ROBOTPROG_AND_START  | 5     | Makes the robot program and starts it on the robot (independently from the PC)        |
#   | RUNMODE_RUN_ROBOT                 | 6     | Moves the real robot from the PC (PC is the client, the robot behaves like a server) |
#
#   Of these, only `RUNMODE_SIMULATE` and `RUNMODE_RUN_ROBOT` are currently
#   supported, with the former being referred to in the UC-02-2024
#   documentation as running _offline_ (simulation only), and the later as
#   _online_ (motion of virtual and physical robots synchronised).
robodk_mode = {
    "offline": 1,
    "online": 6
}

#*****************************************************************************
#   Argument Parsing
#*****************************************************************************

## Instance of the container for argument specifications.
parser = argparse.ArgumentParser(
    ## Name of the program.
    prog = "main",
    ## Text to display before the argument help.
    description = "UC-02-2024 Coffee Cart Modifications demonstration code.",
    ## Text to display after the argument help.
    epilog = "https://gitlab.com/uc_mech_wing/robotics_control_lab/uc-02-2024"
)

# Argument definitions.
parser.add_argument(
    "n",
    ## Restrict permissible argument choices.
    choices = ["0", "1", "2", "3"],
    ## Message displayed if called with -h option.
    help = "Robot / coffee cart number"
)
parser.add_argument(
    "-r",
    choices = ["offline", "online"],
    ## Default is offline for safety reasons.
    default = "offline",
    help = "Run mode (default: offline)"
)
parser.add_argument(
    "-b",
    choices = ["dry", "wet"],
    ## Default is dry to avoid mess.
    default = "dry",
    help = "Brew mode (default: dry)"
)

parser.add_argument(
    "-s",
    choices = ["long", "short"],
    ## Default is short until hot water is sorted.
    default = "short",
    help = "Shot mode (default: short)"
)

## Extract arguments and convert to strings.
args = parser.parse_args()
## Robot / coffee cart number.
robot_number = args.n
## Online or offline.
run_mode = args.r
## Dry or wet.
brew_mode = args.b
## Long or short.
shot_mode = args.s

#*****************************************************************************
#   Scale Initialisation
#
#   If the intent is to brew espresso (i.e. `-r online` and `-b wet`), then
#   this section will create two instances of the Modbus scale client class -
#   one per scale. It then tests to see that contact is able to be made with
#   the respective scale server, and that the servers are receiving data from
#   their scale brokers. If these four conditions are not met, then espresso
#   cannot be made and an error is thrown.
#
#*****************************************************************************

if (run_mode == "online" and brew_mode == "wet"):
    ## Dictionary key.
    scale = f"IP_MAZZER_{robot_number}"
    ## Instance of Modbus Ethernet scale client.
    mazzer_client = modbus_scale_client.ModbusScaleClient(
        host = scale_address[scale])

    scale = f"IP_RANCILIO_{robot_number}"
    ## Instance of Modbus Ethernet scale client.
    rancilio_client = modbus_scale_client.ModbusScaleClient(
        host = scale_address[scale])

    if not (mazzer_client.server_exists() and \
        mazzer_client.broker_exists()):
            sys.exit("Mazzer scale hardware failure")
    
    if not (rancilio_client.server_exists() and \
        rancilio_client.broker_exists()):
            sys.exit("Rancilio scale hardware failure")
else:
    mazzer_client = "NULL"
    rancilio_client = "NULL"

#*****************************************************************************
#   RoboDK Initialisation
#
#   This section establishes a link between RoboDK and Python, and creates a
#   folder in the RoboDK station tree into which all items generated by the
#   preceding code will be positioned.
#
#*****************************************************************************

##
#   @brief Create a link between RoboDK and Python.
#
#   After creating a `Robolink()` object, items within the RoboDK station tree
#   are able to be retrieved by name, item type, or both.
RDK = Robolink()

# Run mode is initially set to "offline", irrespective of the value that was
# passed via the command line. This is because adding frames and targets when
# "online" takes significantly longer than when "offline".
RDK.setRunMode(robodk_mode["offline"])

# Disable rendering until frames and targets have been added to the station
# tree, again to speed up initialisation.
RDK.Render(False)

##
#   @brief Get the robot item by name.
#
#   Note that in this and all subsequent calls to the `Item()` method, both the
#   item `name` and item `type` arguments are used. Providing that all item
#   names within the RoboDK station tree are unique, the second argument is
#   optional. However it is used here out of an abundance of caution and to
#   serve as a visual reminder as to the nature of the object that is being
#   referenced.
UR5 = RDK.Item("UR5", ITEM_TYPE_ROBOT)

## Points class instance.
pts = points.Points(robot_number)
## Frames class instance.
frs = frames.Frames(pts, RDK)
## Targets class instance.
tgs = targets.Targets(frs, RDK)

##
#   @brief Folder that will contain frames and targets.
#
#   By creating a new folder in the station tree and adding frames and targets
#   to the folder, it becomes possible to run code from a clean slate by simply
#   deleting the folder and its contents and recreating it. If however there
#   have been no changes to the frames or targets that it contains, then it is
#   faster to retain the folder and it's contents.
folder = RDK.Item("Frames_Targets", ITEM_TYPE_FOLDER)

if not folder.Valid():
    RDK.Command("AddFolder", "Frames_Targets")
    # Add frames to the station tree.
    frs.add_frames([
        "cup_dispenser",
        "mazzer", "mazzer_scale",
        "puq",
        "rancilio", "rancilio_scale",
        "wdt",
        "rancilio_tool_cleaner",
        "customer_zone",
        ])
    # Add targets to the station tree.
    tgs.add_targets([
        "cup_dispenser:open", "cup_dispenser:shut", "cup_dispenser:cup",
        "mazzer:dose", "mazzer:off", "mazzer:on",
        "mazzer_scale:ball", "mazzer_scale:left", "mazzer_scale:right",
        "puq:clamp",
        "rancilio:switch", "rancilio:gasket",
        "rancilio_scale:pan", "rancilio_scale:left", "rancilio_scale:right",
        "wdt:anvil", "wdt:rotor",
        "rancilio_tool_cleaner:silicone", "rancilio_tool_cleaner:bristle",
        "customer_zone",
        ])
else:
    choice = rd.ShowMessageYesNo("Retain frames and targets?")
    if not choice:
        folder.Delete()
        RDK.Command("AddFolder", "Frames_Targets")
        # Add frames to the station tree.
        frs.add_frames([
            "cup_dispenser",
            "mazzer", "mazzer_scale",
            "puq",
            "rancilio", "rancilio_scale",
            "wdt",
            "rancilio_tool_cleaner",
            "customer_zone",
            ])
        # Add targets to the station tree.
        tgs.add_targets([
            "cup_dispenser:open", "cup_dispenser:shut", "cup_dispenser:cup",
            "mazzer:dose", "mazzer:off", "mazzer:on",
            "mazzer_scale:ball", "mazzer_scale:left", "mazzer_scale:right",
            "puq:clamp",
            "rancilio:switch", "rancilio:gasket",
            "rancilio_scale:pan", "rancilio_scale:left", "rancilio_scale:right",
            "wdt:anvil", "wdt:rotor",
            "rancilio_tool_cleaner:silicone", "rancilio_tool_cleaner:bristle",
            "customer_zone",
            ])

#*****************************************************************************
#   Code
#*****************************************************************************

## Set the run mode.
RDK.setRunMode(robodk_mode[run_mode])
## Tasks class instance.
tks = tasks.Tasks(frs, tgs,
    run_mode, brew_mode, shot_mode,
    mazzer_client, rancilio_client,
    RDK)

# Show selected frames.
frs.set_visible([
#    "cup_dispenser",
#    "mazzer", "mazzer_scale",
#    "puq",
#    "rancilio", "rancilio_scale",
#    "wdt",
#    "rancilio_tool_cleaner",
#    "customer_zone",
    ], True)

# Show selected targets.
tgs.set_visible([
#    "cup_dispenser:open", "cup_dispenser:shut", "cup_dispenser:cup",
#    "mazzer:dose", "mazzer:off", "mazzer:on",
#    "mazzer_scale:ball", "mazzer_scale:left", "mazzer_scale:right",
#    "puq:clamp",
#    "rancilio:switch", "rancilio:gasket",
#    "rancilio_scale:pan", "rancilio_scale:left", "rancilio_scale:right",
#    "wdt:anvil", "wdt:rotor",
#    "rancilio_tool_cleaner:silicone", "rancilio_tool_cleaner:bristle",
#    "customer_zone",
    ], True)

RDK.Render(True)

tks.run_tasks([
    "prep",
    "dose",
    "pack",
    "brew",
    "post",
    ])

