###########################################################################
# EyeRISUDPCLIent.py
#
# Monterey Bay Aquarium Research Institute   2019
# All rights reserved.
#
# Command-line Interface (CLI) client app for the EyeRIS controller.
# Intended to be a temporary client app for the controller while a
# more user-friendly GUI app is developed.
# A number of parameters are hard-coded and intended to be changed
# by the user:
#
# NLights  - currently set to two. Will eventually grow to six.
# UDP_IP   - currently set to 192.168.1.199. Must match controller IP.
# UDP_PORT - currently set to 1111. Must match controller port.
#
# Created:  May    2019      Paul Roberts
# Modified: Jul 26 2019      Rich Henthorn - Initial functionality/formats
#
###########################################################################

import time
import socket
from kbhit import KBHit

# Global variables representing parsed EyeRIS data
eyeTS = eyeSW1 = eyeSW2 = eyeSW3 = eyeSW4 = eyeTEMP1 = eyeHUMID1 = eyePRESS1 = eyeTEMP2 = eyeHUMID2 = eyePRESS2 = eyeVOLT1 = eyeAMP1 = eyeVOLT2 = eyeAMP2 = '0'

# Light level request globals
Levels = ['-', '-', '-', '-']
Light_id = 0
L_id = None
NLights = 2

###########################################################################
# parse_eyeris_string() function.
# Parse the EYERIS string and set new variable values
#
def parse_eyeris_string(eyeris_str):

    global NLights
    global L_id
    global Light_id
    global Levels

    # Split the string by commas
    comp = eyeris_str.split(",")

    # Gotta be at least components
    if (len(comp) < 2):
        return

    # EYERIS data packets begin with '$EYERIS'
    if (comp[0] == '$EYERIS'):

        if (len(comp) < 16):
            print("Invalid EYERIS string: " + eyeris_str)
            return

        global eyeTS
        eyeTS = comp[1]
        
        global eyeSW1
        eyeSW1 = comp[2]
        global eyeSW2
        eyeSW2 = comp[3]
        global eyeSW3
        eyeSW3 = comp[4]
        global eyeSW4
        eyeSW4 = comp[5]

        global eyeTEMP1
        eyeTEMP1  = comp[6]
        global eyePRESS1
        eyePRESS1 = comp[7]
        global eyeHUMID1
        eyeHUMID1 = comp[8]
        
        global eyeTEMP2
        eyeTEMP2  = comp[9]
        global eyePRESS2
        eyePRESS2 = comp[10]
        global eyeHUMID2
        eyeHUMID2 = comp[11]
        
        global eyeVOLT1 
        eyeVOLT1 = comp[12]
        global eyeAMP1 
        eyeAMP1  = comp[13]

        global eyeVOLT2
        eyeVOLT2 = comp[14]
        global eyeAMP2 
        eyeAMP2  = comp[15]

        display_eyeris_data()

        # Request light levels. Sequence through the number of lights
        # Bit of a hack, but the level values are read the next time
        # this function is called - see below
        #
        L_id = Light_id % NLights
        Light_id = Light_id + 1
        if (Light_id == NLights): Light_id = 0
        lreq = "!00" + str(L_id+1) + ":LOUT?"
        client_socket.sendto(lreq.encode(), addr)

    # Light level requests are read here and stored in the levels array
    #
    elif (comp[0] == '$RS485'):

        if (len(comp) < 2):
            print("Unknown RS485 string: " + eyeris_str)
            return

        if (len(comp) == 2):
            Levels[L_id] = comp[1]

# end parse_eyeris_string()        
###########################################################################


###########################################################################
# adorn_pressure() function.
# Int pressure value kPa passed in
# String returned with possible adornments (e.g. "!!" to indicate warning level)
#
def adorn_pressure(pressure_val):
    # Set limits and adorn if limits are exceeded
    PLLIMIT = 80
    PHLIMIT = 120
    adorn = "!!" if (pressure_val < PLLIMIT or pressure_val > PHLIMIT) else ""
    rval = adorn + str(pressure_val) + " kpa" + adorn
    return rval

# end adorn_pressure()        
###########################################################################


###########################################################################
# adorn_temperature() function
# Float temperature value C passed in
# String returned with possible adornments (e.g. "!!" to indicate warning level)
#
def adorn_temperature(temp_val):
    # Set limit and adorn if limit is exceeded
    TLIMIT = 40
    adorn = "!!" if (temp_val > TLIMIT) else ""
    rval = adorn + str(temp_val) + " C" + adorn
    return rval

# end adorn_temperature()        
###########################################################################


###########################################################################
# adorn_humidity()        
# Int humidity value % passed in
# String returned with possible adornments (e.g. "!!" to indicate warning level)
#
def adorn_humidity(humid_val):
    # Set limit and adorn if limit is exceeded
    HLIMIT = 35
    adorn = "!!" if (humid_val > HLIMIT) else ""
    rval = adorn + str(humid_val) + "%" + adorn
    return rval

# end adorn_humidity()        
###########################################################################


###########################################################################
# display_eyeris_data()
# Display the contents of the latest data from the Eyeris to a terminal.
# Also triggers requests for light level data to be read next iteration.
#
def display_eyeris_data():

    # Start with a timestamp
    print(time.ctime(), end='  ')

    # Display switch state on/off as +/-
    s1 = "+" if (eyeSW1 == "On") else "-"
    s2 = "+" if (eyeSW2 == "On") else "-"
    s3 = "+" if (eyeSW3 == "On") else "-"
    s4 = "+" if (eyeSW4 == "On") else "-"
    switches = "SWITCHES:[ " + s1 + "  " + s2 + "  " + s3 + "  " + s4 + " ]"
    print(switches, end='  ')

    # Environmental sensor 1
    temp = float(eyeTEMP1)
    kpa  = int(float(eyePRESS1)/1000.)
    rh   = int(float(eyeHUMID1))
    bme  = "ENV 1:[ " + adorn_temperature(temp) + "  " + adorn_humidity(rh) + "  " + adorn_pressure(kpa) + " ]"
    print(bme, end='  ')

    # Environmental sensor 2
    temp = float(eyeTEMP2   )
    kpa  = int(float(eyePRESS2)/1000.)
    rh   = int(float(eyeHUMID2))
    bme  = "ENV 2:[ " + adorn_temperature(temp) + "  " + adorn_humidity(rh) + "  " + adorn_pressure(kpa) + " ]"
    print(bme, end='  ')

    # Both power sensors
    pwr = "PWR 1:[ " + eyeVOLT1 + " V  " + eyeAMP1 + " A ]  PWR 2:[ " + eyeVOLT2 + " V  " + eyeAMP2 + " A ]"
    print(pwr, end='  ')

    # Light levels returned from light level requests in parse_eyeris_string()
    lights = "LIGHTS: [ " + Levels[0] + " " + Levels[1] + " " + Levels[2] + " " + Levels[3] + " ]"
    print(lights)

# end display_eyeris_data()
###########################################################################


###########################################################################
# switch_menu()
# Get the light switch command.
# Returns "none" on invalid input (e.g., no input)
# Otherwies, returns a valid command string (e.g., "FFR0011") that can be
# sent to the controller.
#
def switch_menu():
    rval = "none"

    switches = ["0", "0", "0", "0"]

    switches[0] = "1" if (eyeSW1 == 'On') else "0"
    switches[1] = "1" if (eyeSW2 == 'On') else "0"
    switches[2] = "1" if (eyeSW3 == 'On') else "0"
    switches[3] = "1" if (eyeSW4 == 'On') else "0"

    l_id = input("Switch number [1..4] > ")
    if (len(l_id) <= 0):
        return rval

    try:
        i = int(l_id)
        if (i < 1 or i > 4):
            return rval
        else:
            l_id = str(i)

    except ValueError:
        return rval

    val  = input("O or 1 > ")
    if (len(val) <= 0):
        return rval

    if (val != "1"):
        val = "0"

    # Set the switch for the command string
    switches[i-1] = val

    # Build the command string
    rval = "FFR" + switches[0] + switches[1] + switches[2] + switches[3]
    return rval

# end switch_menu()
###########################################################################

###########################################################################
# light_menu()
# Get the light level command.
# Returns "none" on invalid input (e.g., no input)
# Otherwies, returns a valid command string (e.g., "!002:LOUT=30") that can be
# sent to the controller.
#
def light_menu():
    rval = "none"

    # Get the light id
    l_id = input("Light number [1..2] > ")
    if (len(l_id) <= 0):
        return rval

    # Must be an int
    try:
        i = int(l_id)
        if (i < 1 or i > 2):
            return rval
        else:
            l_id = str(i)     # This handles blanks in user input (e.g., " 1 ")

    except ValueError:
        return rval

    # Get light level (0 to 100)
    val  = input("O to 100 > ")
    if (len(val) <= 0):
        return rval

    try:
        i = int(val)
        # Enforce 0 to 100 limit
        if (i < 0):
            i = 0

        if (i > 100):
            i = 100

        val = str(i)           # Handle blanks in user input (e.g., "   20 ")

    except ValueError:
        return rval

    # Build the command string
    rval = "!00" + l_id + ":LOUT=" + val
    return rval

# end light_menu()
###########################################################################


###########################################################################
# main_menu()
# Get the controller command.
# Returns "none" on invalid input (e.g., no input)
# Otherwies, returns a valid command string (e.g., "!002:LOUT=30") that can be
# sent to the controller.
#
def main_menu():
    global kb
    rval = "none"

    cmd = input('(A)ll off  (S)witches  (L)ights > ')

    if (len(cmd) < 1):                        # User just pressed enter
        rval = "none"

    elif (cmd[0] == 'a' or cmd[0] == 'A'):    # All components off
        rval = "FFR0000"

    elif (cmd[0] == 's' or cmd[0] == 'S'):    # Toggle a switch
        rval = switch_menu()

    elif (cmd[0] == 'l' or cmd[0] == 'L'):    # Set light levels
        rval = light_menu()

    return rval

# end main_menu()
###########################################################################


###########################################################################
# command_menu()
# Get a command from the main menu.
# Returns "none" if no valid command is provided.
# Otherwies, returns the command string (e.g., "!002:LOUT=30") that can be
# sent to the controller.
#
def command_menu():
    cmd = main_menu()
    print(cmd)
    return cmd

# end command_menu()
###########################################################################


###########################################################################
#  Main program
###########################################################################

UDP_IP = "192.168.1.199"
UDP_PORT = 1111
STATUS_INTERVAL = 1500

# Open socket connection to controller
client_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
client_socket.settimeout(0.25)
addr = (UDP_IP, UDP_PORT)

# Create log file name
fname = time.ctime()
fname = fname.replace(' ', '_')
fname = fname.replace(':', '')
LOG_FILE = 'logs\\' + fname + '.log'

# send an interval command to start get things started
client_socket.sendto(("FFS" + str(STATUS_INTERVAL)).encode(), addr)

# Banner

print("###########################################################################")
print("")
print("                 EyeRIS UDP Command-line CLIent")
print("            Monterey Bay Aquarium Research Institute   2019")
print("")
print("            - Stretch window width for better viewing")
print("")
print("            - ESC to exit")
print("")
print("            - ENTER to get the command menu")
print("")
print("            - Any other key to enter a direct controller command")
print("")
print("            - ESC to exit")
print("")
print("            - Log file: " + LOG_FILE)
print("")
print("###########################################################################")

kb = KBHit()
while True:

    kb.set_new_term()

    # check for user input
    if kb.kbhit():
        c = kb.getch()
        kb.set_normal_term()
        if (ord(c) == 27): # ESC
            # turn off status messages before exit
            client_socket.sendto("FFS0".encode(), addr)
            print("")
            break
        elif (ord(c) == 10 or ord(c) == 13): # CR
            print("\r\nGetting menu command...")
            cmd = command_menu()
            if (cmd != 'none'):    # Not a null command
                client_socket.sendto(cmd.encode(), addr)

        else:
            cmd = input("\r\nCMD: ")
            client_socket.sendto(cmd.encode(), addr)

        print("")
        print("")


    try:
        data, server = client_socket.recvfrom(1024)
        utf8_data = data.decode('utf-8').rstrip('\r\n')

        parse_eyeris_string(utf8_data) ## print(utf8_data, end = '\r')

        with open(LOG_FILE,'a+') as f:
            f.write(str(time.ctime()) + ' : ' + utf8_data + '\n')
            #f.write(str(time.time()) + ' : ' + lreq + '\n')
    except socket.timeout:
        pass
        #print('REQUEST TIMED OUT')

    time.sleep(0.200)

# end main
###########################################################################
