# Robi_Ui.py
#
# python program to REST-ful connect to Robi's web server to load parameters
#  and control the walking pattern
#
# The software is provided as is with no warranty nor liability, use at your own risk.
# The software is a starting point to remote control a robot and is by no means a complete robot remote controller.
# It is shared with the hope to be a helpful resource for your robot projects.
#
# inital version 20-Jun-2022 by wolf2018
# 3-Aug-2022: implement Battery voltage, walking planning data, walking parameters, debug levels
# 12-Feb-2023: implement gracefully close the server connection
#             implement variables for values a user might like to change
# 14-Feb-2023: implement lifetick
# 19-Feb-2023: clean_up unused code, test for first release
# 25-Feb-2023: adjust UI functions to shutdown sequence of robot controller
#
# environment is python 3.9.7 [py397]
# last update: see version_date


from tkinter import *
from tkinter import ttk
import asyncio
import aiohttp
import json
import sys


version_date = "25-Feb-2023"

# debug has levels: 0 limited printout, 1 medium informative, level 2 all printouts
debug = 2

# define server address and start event loop

server = "http://192.168.1.79:8081"

loop = asyncio.new_event_loop()

# Define variables and constants a user might like to change

# min/max values for target distance to walk
target_distance_max = 20
step_width_max = 4

# time interval between life ticks in seconds
lifetick_time = 3

# control variables
lifetick_LED_on = False
shut_down = False
connected = False


# Define dictionaries to hold the data to be transferred to Robi
# and set default values
parameter_dict = {"m_step": 0.20,
                "friction": 0.00,
                "delay": 30,
                "CoM": 10.5}

walking_dict = {"target": 8,
                "step": 2,
                "leading": "L",
                "action": "init"}

parameter_actual = {"m_step": 0,
                "friction": 0,
                "delay": 0,
                "CoM": 0}

walking_actual = {"target": 0,
                "step": 0,
                "leading": "none",
                "action": "init"}

Robi_status_dict = {"html": 0,
                    "message": "not initialized",
                    "engine_on": False}


#
# define co-routines and callbacks
#
async def lifetick():
    """lifetick is a routine which continuously calls Robi to get status"""
    # set time interval for life tick in variable lifetick_time above

    global lifetick_LED_on, shut_down, connected

    # send lifetick if robot controller is life and no shutdown in progress
    if not shut_down and Robi_status_dict["engine_on"] and connected:

        if debug > 1:
            print("shutdown = ", shut_down, "    robot controller on= ", Robi_status_dict["engine_on"])
            print("...sending life tick...")
        conn_response = await send_get()
        conn_resp_dict = json.loads(conn_response)

        # load UI fields with values received
        Robi_status.set(conn_resp_dict["message"])
        command.set(conn_resp_dict["action"])
        display_batt(conn_resp_dict["battery"])
        Robi_memory.set(conn_resp_dict["memory"])

        # life tick can be scheduled via tkinter, but there is no control

        # lbl_Robi_lifetick.after(lifetick_time*1000, lambda: asyncio.ensure_future(lifetick()))
        # by the main program when doing so.
        # Better: See run_tk() to manage life tick in main loop

        # switch label style every life tick
        if not lifetick_LED_on:
            lbl_Robi_lifetick.configure(style="lifetick_on.TLabel")
            lifetick_LED_on = True
        else:
            lbl_Robi_lifetick.configure(style="lifetick_off.TLabel")
            lifetick_LED_on = False

    else:
        if debug > 0:
            print("disconnected..... initialize first")
        lbl_Robi_lifetick.configure(style="lifetick_grey.TLabel")


async def send_get():
    """GET request to server and return server response to the caller"""

    if debug > 0:
        print("send_get called")
    async with session.get(server) as resp:
        if debug > 0:
            print(resp.status)
        Robi_status_dict["html"] = resp.status
        server_response = await resp.text()
        if debug > 0:
            print("server response is: ", server_response)
        return server_response


async def send_put(out_data={"test": "test_data"}):
    """sends a PUT request to the server.
    -- expects a dictionaly as argument to send json formatted
    -- returns the server response"""

    payload = json.dumps(out_data)
    if debug > 0:
        print("send data is: ", payload)

    async with session.put(server, json=payload) as resp:
        Robi_status_dict["html"] = resp.status
        server_response = await resp.text()
        if debug > 1:
            print(resp.status)
            print(server_response, type(server_response))
        return server_response


async def send_post(out_data={"test": "test_data"}):
    """sends a POST request to the server.
    -- expects a dictionaly as argument to send json formatted
    -- returns the server response"""

    payload = json.dumps(out_data)
    if debug > 0:
        print("send data is: ", payload)

    async with session.post(server, json=payload) as resp:
        if debug > 0:
            print(resp.status)
        Robi_status_dict["html"] = resp.status
        server_response = await resp.text()
        if debug > 0:
            print(server_response, type(server_response))
        return server_response


async def run_tk(root, interval=0.045):
    '''
    Run tkinter app update in the asyncio event loop every ~50ms
    '''
    global tick_counter, lifetick_time, connected, shut_down
    tick_counter = 0
    try:
        while True:
            # update data variables
            update_actuals()
            # update data on screen
            root.update()

            # run life tick every n intervals of main loop
            # n is determined by settings of interval and lifetick_time
            tick_counter += 1
            lifetick_conditions = [tick_counter > int(lifetick_time/interval), Robi_status_dict["engine_on"], not shut_down]
            if all(lifetick_conditions):
                await lifetick()
                tick_counter = 0

            # pause main loop for
            await asyncio.sleep(interval)

    except TclError as e:

        if debug > 1:
            print("Tcl Exception received: \n" + str(e))

        raise

################################################
# Ui related call backs


def display_batt(volt):
    """function to set battery voltage window color based on battery voltage ( ~charge status) and display voltage with two digits"""

    # set color vs battery voltage below
    batt_green = 3.85  # charge ~> 100...70%
    batt_yellow = 3.6  # charge ~> 70...50%
    batt_orange = 3.3     # charge ~> 50...20%
    # batt_red <= 3.3, charge ~< 20%

    batt_voltage = str("{: .2f}".format(volt))
    Robi_batt_voltage.set(batt_voltage)

    if volt > batt_green:
        lbl_Robi_batt_voltage_value.configure(style="Batt_green.TLabel")

    elif volt > batt_yellow:
        lbl_Robi_batt_voltage_value.configure(style="Batt_yellow.TLabel")

    elif volt > batt_orange:
        lbl_Robi_batt_voltage_value.configure(style="Batt_amber.TLabel")

    else:
        lbl_Robi_batt_voltage_value.configure(style="Batt_red.TLabel")


def change_server():
    """ on entry of IP:port change the serve address """
    global server
    server = interface.get()
    if debug > 0:
        print("server is now: ", server)


async def connect_cb():
    """ transfers Parameters to Robi, then sends init command and get status back and display"""
    global shut_down, connected
    shut_down = False
    if debug > 0:
        print("connecting. to robot controller...")

    Robi_status_dict["engine_on"] = True
    connected = True

    await transfer_cb()    # inital parameter transfer
    await command_cb("init")    # initialize Robi to stand upright


async def disconnect_cb():
    """ disconnect routine stops life tick and resets values"""
    global connected

    if debug > 0:
        print("disconnected and reset values....")

    # update UI display values
    clear_values()

    # signal robot controller is disconnected
    connected = False
    Robi_status.set("disconnected")


async def command_cb(_cmd):
    """sends a POST request to the server.
    -- expects a dictionary as argument to send data json formatted
    -- loads status fields based on server response"""
    global shut_down, connected

    if not shut_down and connected:

        walking_dict["action"] = _cmd
        if debug > 0:
            print("\ncommand is: "+_cmd)

        # signal to lifetick that a stop command is issued and controller will shut down soon
        if _cmd == "stop":
            shut_down = True
            if debug > 1:
                print("set shut_down to: ", shut_down)

        out_data = {}
        out_data["action"] = _cmd
        payload = json.dumps(out_data)
        if debug > 0:
            print("send data is: ", payload)
        walking_status.set(_cmd+" requested")

        async with session.post(server+"/actions", json=payload) as resp:
            if debug > 0:
                print("http status is: ", resp.status)
            Robi_status_dict["html"] = resp.status
            if Robi_status_dict["html"] == 201:
                walking_status.set(_cmd+" requested")

            server_response = await resp.text()

            if debug > 0:
                print("server response: ", server_response, type(server_response))

            resp_dict = json.loads(server_response)

            # load UI status fields with values received
            Robi_status.set(resp_dict["message"])
            command.set(resp_dict["action"])
            display_batt(resp_dict["battery"])
            Robi_memory.set(resp_dict["memory"])
            walking_status.set(resp_dict["w_status"])

            if "stop" in resp_dict["w_status"]:
                Robi_status_dict["engine_on"] = False
                if debug > 0:
                    print("robot controller has stopped....")
                clear_values()

    else:
        if debug > 0:
            print("disconnected..... initialize first")


def micro_steps_cb(value):
    _pvalue = value[0:4]
    lbl_micro_steps_value.configure(text=_pvalue)
    parameter_dict["m_step"] = _pvalue
    # print("parameter micro steps changed"+_pvalue)


def friction_cb(value):
    _pvalue = value[0:4]
    lbl_friction_value.configure(text=_pvalue)
    parameter_dict["friction"] = _pvalue
    # print("parameter friction changed"+_pvalue)


def delay_cb(value):
    _pvalue = value[0:4]
    lbl_delay_value.configure(text=_pvalue)
    parameter_dict["delay"] = _pvalue
    # print("parameter delay changed"+_pvalue)


def CoM_cb(value):
    _pvalue = value[0:4]
    lbl_CoM_value.configure(text=_pvalue)
    parameter_dict["CoM"] = _pvalue
    # print("parameter CoM changed"+_pvalue)


def target_cb(value):
    _pvalue = value[0:4]
    lbl_target_value.configure(text=_pvalue)
    walking_dict["target"] = _pvalue
    # print("target changed to "+_pvalue)


def step_cb(value):
    _pvalue = value[0:4]
    lbl_step_width_value.configure(text=_pvalue)
    walking_dict["step"] = _pvalue
    # print("step changed to "+_pvalue)


async def transfer_cb():
    """async function to transfer parameters to Robi and updates UI actual parameters with responses from Robi"""
    global shut_down, connected
    if not shut_down and connected:
        if debug > 1:
            print("button transfer pressed")
        response = await send_post(parameter_dict)
        data = json.loads(response)
        # print(data, type(data))
        parameter_actual["m_step"] = data["m_step"]
        parameter_actual["friction"] = data["friction"]
        parameter_actual["delay"] = data["delay"]
        parameter_actual["CoM"] = data["CoM"]
        print(parameter_actual)

    else:
        if debug > 0:
            print("disconnected .... initilize first")


async def load_cb():
    """function to transfer walking data to Robi async and updates actuals dictionary with responses from Robi"""
    global shut_down, connected
    if not shut_down and connected:
        response = await send_put(walking_dict)
        data = json.loads(response)
        if debug > 1:
            print("got data back: ", data, type(data))
        walking_actual["target"] = data["target"]
        walking_actual["step"] = data["step"]
        walking_actual["leading"] = data["leading"]
        walking_actual["action"] = data["action"]

    else:
        if debug > 0:
            print("disconnected .... initialize first")


def leading_leg_cb(_leg):
    if _leg == "left":
        walking_dict["leading"] = "L"
    elif _leg == "right":
        walking_dict["leading"] = "R"
    else:
        print("no leg selected")

    if debug > 1:
        print("leg selected: "+_leg)


def update_actuals():
    # update UI display values
    lbl_micro_steps_actual.configure(text=parameter_actual["m_step"])
    lbl_friction_actual.configure(text=parameter_actual["friction"])
    lbl_delay_actual.configure(text=parameter_actual["delay"])
    lbl_CoM_actual.configure(text=parameter_actual["CoM"])
    # update UI actual walking parameters
    lbl_target_actual.configure(text=walking_actual["target"])
    lbl_step_width_actual.configure(text=walking_actual["step"])
    lbl_leading_actual.configure(text=walking_actual["leading"])
    # Robi status updates
    Conn_status.set(Robi_status_dict["html"])


def clear_values():
    # reset Ui values
    if debug > 1:
        print("Clearing values....")
    parameter_actual["m_step"] = 0.00
    parameter_actual["friction"] = 0
    parameter_actual["delay"] = 0.00
    parameter_actual["CoM"] = 0
    # update UI actual walking parameters
    walking_actual["target"] = 0
    walking_actual["step"] = 0
    walking_actual["leading"] = "L"
    # Robi status updates
    Robi_status_dict["html"] = 0
    Robi_batt_voltage.set(0.00)
    Robi_memory.set(0.00)
    function.set("disconnect")


################################################
#
# tkinter definition for user Interface
#
# define root Window:


root = Tk()
root.title("Robi User Interface     version: "+version_date)

#
# set UI styles for the different widgets

style_d = ttk.Style()

style_d.configure("TFrame", background="#E7EAEF")
style_d.configure("Parameters.TFrame", background="#FFFFFF")
style_d.configure("Walking.TFrame", background="#AAFFAA")
style_d.configure("Parameters.TLabel", font=("Arial", 10, "bold"), foreground="blue", background="#FFFFFF")
style_d.configure("ParametersHeading.TLabel", font=("Arial", 11, "bold"), foreground="blue", background="#CCDDFF")
style_d.configure("TEntry", foreground="blue", background="#AECEFF")
style_d.configure("TButton", padding=2, background="#aeceff", font=("Arial", 10, "bold"))
style_d.configure("Error.TButton", padding=2, foreground="white", background="#FF2222", font=("Arial", 10, "bold"))
style_d.configure("TRadiobutton", padding=2, font=("Arial", 10, "bold"))
style_d.configure("red.TRadiobutton", padding=2, font=("Arial", 10, "bold"), foreground="red")
style_d.configure("TSpinbox", foreground="blue")
style_d.configure("lbl_red.TLabel", foreground="black", background="red")
style_d.configure("lbl_yellow.TLabel", foreground="black", background="yellow", relief="sunken", font=("Arial", 10, "bold"))
style_d.configure("lbl_green.TLabel", foreground="white", background="#303088", font=("Arial", 10, "bold"), borderwidth=1)
style_d.configure("Batt_green.TLabel", foreground="green", background="#BBFF9F", border=3)
style_d.configure("Batt_yellow.TLabel", foreground="black", background="yellow", border=3)
style_d.configure("Batt_amber.TLabel", foreground="green", background="orange", border=3)
style_d.configure("Batt_red.TLabel", foreground="white", background="red", border=3)
style_d.configure("lifetick_on.TLabel", foreground="white", background="green", border=3)
style_d.configure("lifetick_off.TLabel", foreground="green", background="white", border=3)
style_d.configure("lifetick_grey.TLabel", foreground="grey", background="white", border=3)


# main frame to hold user interface
mainframe = ttk.Frame(root, padding="20 20 20 20")
mainframe.grid(column=0, row=0, sticky=(N, W, E, S))
root.columnconfigure(0, weight=1)
root.rowconfigure(0, weight=1)


# UI consists of five columns and is built up by row
# typically _row increments relative to the previous row

# Robi connection
_row = 0
lbl_port = ttk.Label(mainframe, text="Enter Robi's        IP-address:port ").grid(column=0, row=_row, columnspan=3, sticky=W)
interface = StringVar()
interface.set(server)
interface_entry = ttk.Entry(mainframe, width=30, textvariable=interface, font=("Arial", 10, "bold"), background="#AECEFF").grid(column=2, row=_row, columnspan=3, sticky=(W))
btn_set = ttk.Button(mainframe, text="set IP:port", command=change_server)
btn_set.grid(column=4, row=_row, sticky=E)

_row += 1
lbl_Robi_conn = ttk.Label(mainframe, text="Robi connection:").grid(column=0,
        row=_row, sticky=W)

function = StringVar()
function0 = ttk.Radiobutton(mainframe, text="initialize", variable=function, value="connect", command=lambda: asyncio.ensure_future(connect_cb()))
function0.grid(column=1, row=_row, sticky=(W, E))
function1 = ttk.Radiobutton(mainframe, text="disconnect", variable=function, value="disconnect", command=lambda: asyncio.ensure_future(disconnect_cb()))
function1.grid(column=2, row=_row, sticky=(W, E))


lbl_Conn_status = ttk.Label(mainframe, text="last http status:").grid(column=4, row=_row, sticky=E)
Conn_status = StringVar()
Conn_status.set("not connected")
lbl_Conn_status_value = ttk.Label(mainframe, textvariable=Conn_status, style="lbl_green.TLabel").grid(column=5, row=_row, padx=5, pady=10, sticky=W)


# Robi status
_row += 1
lbl_Robi_status = ttk.Label(mainframe, text="Robi status msg:").grid(column=0, row=_row, sticky=W)

Robi_status = StringVar()
Robi_status.set(Robi_status_dict["message"])
lbl_Robi_status_value = ttk.Label(mainframe, textvariable=Robi_status, style="lbl_green.TLabel").grid(column=1, row=_row, padx=5, pady=10, columnspan=3, sticky=W)

lbl_Robi_battery = ttk.Label(mainframe, text="Battery voltage:").grid(column=4, row=_row, padx=5, sticky=E)

Robi_batt_voltage = StringVar()
Robi_batt_voltage.set(0.00)
lbl_Robi_batt_voltage_value = ttk.Label(mainframe, textvariable=Robi_batt_voltage, style="Batt_red.TLabel")
lbl_Robi_batt_voltage_value.grid(column=5, row=_row, padx=5, pady=10, sticky=W)

_row += 1
lbl_Robi_lifetick = ttk.Label(mainframe, text="life tick", style="yellow.TLabel")
lbl_Robi_lifetick.grid(column=0, row=_row, padx=5, sticky=W)

lbl_Robi_memory = ttk.Label(mainframe, text="free memory:").grid(column=4, row=_row, padx=5, sticky=E)

Robi_memory = StringVar()
Robi_memory.set(0.00)
lbl_Robi_memory_value = ttk.Label(mainframe, textvariable=Robi_memory, style="Parameters.TLabel")
lbl_Robi_memory_value.grid(column=5, row=_row, padx=5, pady=10, sticky=W)


# Create frame for action buttons and walking planning entry
# left columns are entry, right column shows currently used parameters

_row += 1
walking_frame = ttk.Frame(mainframe, padding="5 10 5 20", style="Walking.TFrame")
walking_frame.grid(column=0, row=_row, columnspan=6, rowspan=5, sticky=(N, W, E, S))
_row += 1
lbl_walking = ttk.Label(walking_frame, text="Robi actions and walking planning values", style="Parameters.TLabel").grid(column=1, row=_row, columnspan=4, pady=10, sticky=W)

_row += 1
# Robi commands
_row += 1
lbl_Robi_command = ttk.Label(walking_frame, text="Robi action:", style="lbl_green.TLabel").grid(column=0, row=_row, padx=5, sticky=E)
command = StringVar()
command0 = ttk.Radiobutton(walking_frame, text="stop", style="red.TRadiobutton", variable=command, value="stop", command=lambda: asyncio.ensure_future(command_cb("stop"))).grid(column=1, row=_row, sticky=(W, E))
command1 = ttk.Radiobutton(walking_frame, text="ready", variable=command, value="ready", command=lambda: asyncio.ensure_future(command_cb("ready"))).grid(column=2, row=_row, sticky=(W, E))
command2 = ttk.Radiobutton(walking_frame, text="walk", variable=command, value="walk", command=lambda: asyncio.ensure_future(command_cb("walk"))).grid(column=3, row=_row, sticky=(W, E))
command3 = ttk.Radiobutton(walking_frame, text="stand", variable=command, value="stand", command=lambda: asyncio.ensure_future(command_cb("stand"))).grid(column=4, row=_row, sticky=(W, E))
walking_status = StringVar()
walking_status.set("not defined")
lbl_walking_status_value = ttk.Label(walking_frame, textvariable=walking_status, style="lbl_green.TLabel").grid(column=5, row=_row, padx=5, sticky=W)

_row += 1
lbl_walking_entry = ttk.Label(walking_frame, text="Robi walking planning: ", style="Parameters.TLabel").grid(column=1, row=_row, columnspan=4, pady=10, sticky=W)
lbl_walking_actual = ttk.Label(walking_frame, text="actual values:", style="Parameters.TLabel").grid(column=5, row=_row, pady=10, padx=5, sticky=E)

_row += 1
btn_load = ttk.Button(walking_frame, text="load values-->", command=lambda: asyncio.ensure_future(load_cb()))
btn_load.grid(column=3, row=_row, rowspan=3, sticky=E)

# walking parameter sliders
lbl_target = ttk.Label(walking_frame, text="target distance [cm]", style="Parameters.TLabel").grid(column=0, row=_row, pady=5, sticky=W)
target = DoubleVar()
target.set(walking_dict["target"])
target_entry = ttk.Scale(walking_frame, orient=HORIZONTAL, length=100, from_=1, to=target_distance_max, variable=target, command=target_cb)
target_entry.grid(column=1, row=_row, sticky=(W))
lbl_target_value = ttk.Label(walking_frame, text=str(target_distance_max))
lbl_target_value.grid(column=2, row=_row, sticky=W)
lbl_target_actual = ttk.Label(walking_frame, text="0.00")
lbl_target_actual.grid(column=5, row=_row, padx=5)

_row += 1
lbl_step_width = ttk.Label(walking_frame, text="single step width [cm]:", style="Parameters.TLabel").grid(column=0, row=_row, sticky=E)
step_width = DoubleVar()
step_width.set((walking_dict["step"]))
step_width_entry = ttk.Scale(walking_frame, orient=HORIZONTAL, length=100, from_=1, to=step_width_max, variable=step_width, command=step_cb).grid(column=1, row=_row, sticky=(W))
lbl_step_width_value = ttk.Label(walking_frame, text=str(step_width_max))
lbl_step_width_value.grid(column=2, row=_row, sticky=W)
lbl_step_width_actual = ttk.Label(walking_frame, text="0.00")
lbl_step_width_actual.grid(column=5, row=_row)

_row += 1

# select leading leg
lbl_leading_leg = ttk.Label(walking_frame, text="leading leg:", style="Parameters.TLabel").grid(column=0, row=_row, sticky=W)
leg = StringVar()
leg.set("left")
leg0 = ttk.Radiobutton(walking_frame, text="left", variable=leg, value="left", command=lambda: leading_leg_cb("left")).grid(column=1, row=_row, sticky=(W, E))
leg1 = ttk.Radiobutton(walking_frame, text="right", variable=leg, value="right", command=lambda: leading_leg_cb("right")).grid(column=2, row=_row, sticky=(W, E))
lbl_leading_actual = ttk.Label(walking_frame, text="none")
lbl_leading_actual.grid(column=5, row=_row)


# Create frame and sliders for the parameters
# parameters are typically set once for a Robot, can be adjusted as needed
# left columns are entry, right column shows currently used parameters

_row += 2
parameters = ttk.Frame(mainframe, padding="5 20 5 20", style="Parameters.TFrame")
parameters.grid(column=0, row=_row, columnspan=6, rowspan=5, sticky=(N, W, E, S))
_row += 1
lbl_Parameters = ttk.Label(parameters, text="Robi Parameters: ", style="ParametersHeading.TLabel").grid(column=1, row=_row, columnspan=4, pady=10, sticky=W)

_row += 1
lbl_Parameters = ttk.Label(parameters, text="Use sliders to adjust, then press transfer", style="Parameters.TLabel").grid(column=0, row=_row, columnspan=4, pady=10, sticky=W)
lbl_Parameters_actual = ttk.Label(parameters, text="Robi parameters:", style="Parameters.TLabel").grid(column=5, row=_row)

_row += 1
btn_transfer = ttk.Button(parameters, text="transfer-->", command=lambda: asyncio.ensure_future(transfer_cb()))
btn_transfer.grid(column=3, row=_row, rowspan=4, padx=10)

# parameter sliders
lbl_micro_step = ttk.Label(parameters, text="micro step width [cm]:", style="Parameters.TLabel").grid(column=0, row=_row, sticky=E)
micro_steps = DoubleVar()
micro_steps.set(0.2)
micro_steps_entry = ttk.Scale(parameters, orient=HORIZONTAL, length=100, from_=0.1, to=1.0, variable=micro_steps, command=micro_steps_cb).grid(column=1, row=_row, sticky=(W))
lbl_micro_steps_value = ttk.Label(parameters, text="0.20")
lbl_micro_steps_value.grid(column=2, row=_row, sticky=W)
lbl_micro_steps_actual = ttk.Label(parameters, text="0.00")
lbl_micro_steps_actual.grid(column=5, row=_row)

_row += 1
lbl_friction = ttk.Label(parameters, text="friction [%]:", style="Parameters.TLabel").grid(column=0, row=_row, sticky=E)
friction = DoubleVar()
friction_entry = ttk.Scale(parameters, orient=HORIZONTAL, length=100, from_=0.0, to=1.0, variable=friction, command=friction_cb).grid(column=1, row=_row, sticky=(W))
lbl_friction_value = ttk.Label(parameters, text="0.00")
lbl_friction_value.grid(column=2, row=_row, sticky=W)
lbl_friction_actual = ttk.Label(parameters, text="0.00")
lbl_friction_actual.grid(column=5, row=_row)

_row += 1
lbl_para_delay = ttk.Label(parameters, text="Delay [ms]: ", style="Parameters.TLabel").grid(column=0, row=_row, sticky=E)
delay = DoubleVar()
delay.set(30)
delay_entry = ttk.Scale(parameters, orient=HORIZONTAL, length=100, from_=10, to=50, variable=delay, command=delay_cb).grid(column=1, row=_row, sticky=(W))
lbl_delay_value = ttk.Label(parameters, text="30")
lbl_delay_value.grid(column=2, row=_row, sticky=W)
lbl_delay_actual = ttk.Label(parameters, text="0.00")
lbl_delay_actual.grid(column=5, row=_row)

_row += 1
lbl_CoM = ttk.Label(parameters, text="Center of Mass height:", style="Parameters.TLabel").grid(column=0, row=_row, sticky=E)
CoM = DoubleVar()
CoM.set(10.5)
# CoM could be from 6 to 10.7 for Robi, starting a 9 is more reasonable
CoM_entry = ttk.Scale(parameters, orient=HORIZONTAL, length=100, from_=9, to=10.7, variable=CoM, command=CoM_cb).grid(column=1, row=_row, sticky=(W))
lbl_CoM_value = ttk.Label(parameters, text="10.5")
lbl_CoM_value.grid(column=2, row=_row, sticky=W)
lbl_CoM_actual = ttk.Label(parameters, text="0.00")
lbl_CoM_actual.grid(column=5, row=_row)


#
# main program starts here
#


async def main():
    """ main sloop, starts web session and tkinter UI"""

    try:
        # create session globally
        global session, loop
        session = aiohttp.ClientSession()

        print("client session initialized")

        #
        print("\nStarting tkinter UI\n")
        await run_tk(root)

    except TclError as e:
        if "application has been destroyed" in e.args[0]:
            print("UI window closed, exiting")
            sys.exit()
        else:
            if debug > 1:
                print("Tcl exception:\n", e)

    except Exception as e:
        print("Application closed with exception: " + str(e) + "\n")

    finally:
        # close web Clinet session
        print("finally: closing web session...")
        await session.close()

        if debug > 0:
            print("closing main loop")
        loop.stop()
        sys.exit(0)

loop = asyncio.get_event_loop()
if debug > 1:
    print("asyncio loop is: ", loop)

asyncio.run(main())
