'''
                NANO ESP32
                   ____
              ____|____|____            |R P20 (D14)
             |       +------|---------> |G P21 (D15)
       LED -+| D13  _|_ D12 |+- P13     |B P22 (D16)
          <-+| 3V3  RGB D11 |+- P12
           -+| B0       D10 |+- P11
       P14 -+| D17(A0)   D9 |+- P10
       P15 -+| D18(A1)   D8 |+- P9
       P16 -+| D19(A2)   D7 |+- P8
       P17 -+| D20(A3)   D6 |+- P7
       P18 -+| D21(SDA)  D5 |+- P6
       P19 -+| D22(SCL)  D4 |+- P5
   P23/P24 -+| D23(A6)   D3 |+- P4
           -+| D24(A7)   D2 |+- P3
          <-+| 5V       GND |+-
           -+| B1      -RST |+-
           -+| GND       D0 |+- P2
          >-+| VIN       D1 |+- P1
             |______________|

+=======+===+===+===+===+===+===+===+===+===+===+===+===+===+===+===+===+===+===+===+===+===+===+===+===+
| pinID |  1|  2|  3|  4|  5|  6|  7|  8|  9| 10| 11| 12| 13| 14| 15| 16| 17| 18| 19| 20| 21| 22| 23| 24|
+=======+===+===+===+===+===+===+===+===+===+===+===+===+===+===+===+===+===+===+===+===+===+===+===+===+
| GPIO  | 43| 44|  5|  6|  7|  8|  9| 10| 17| 18| 21| 38| 47|  1|  2|  3|  4| 11| 12| 46|  0| 45| 13| 13|
| DX    |  1|  0|  2|  3|  4|  5|  6|  7|  8|  9| 10| 11| 12| 17| 18| 19| 20| 21| 22| 14| 15| 16| 23| 23|
| AX    |  -|  -|  -|  -|  -|  -|  -|  -|  -|  -|  -|  -|  -|  0|  1|  2|  3|  6|  7|  -|  -|  -|  6|  6|
+-------+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
| Tipo  |  O|  O|  O|  O|  O|  O|  O|  O|  O|  O|  O|  O|  O|  O|  O|  O|  O|  O|  O| O*| O*| O*|  -|  -|
|       |  I|  I|  I|  I|  I|  I|  I|  I|  I|  I|  I|  I|  I|  I|  I|  I|  I|  I|  I|  -|  -|  -|  -|  -|
|       |  -|  -|  -|  -|  -|  -|  -|  -|  -|  -|  -|  -|  -|  -|  -|  A|  A|  A|  A|  -|  -|  -| A*| A*|
|       |  P|  P|  P|  P|  P|  P|  P|  P|  P|  P|  P|  P|  P|  P|  P|  P|  P|  P|  P|  -|  -|  -|  -|  -|
|       |  S|  S|  S|  S|  S|  S|  S|  S|  S|  S|  S|  S|  S|  S|  S|  S|  S|  S|  S|  -|  -|  -|  -|  -|
+-------+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+

O* Salidas internas al led RGB: Pin20=RED, Pin21=GREEN, Pin22=BLUE
A* Entrada analógica asociada a un sensor DHT22 (DX=23, AX=6, GPIO=13).
pinID = 23 -> sensor de temperatura DHT22.
pinID = 24 -> sensor de humedad DHT22.
'''

from binascii import crc32
import dht
from machine import ADC, idle, Pin, PWM, Timer, reset
import network
import requests
import socket
from time import sleep


# Modelo del módulo.
MODULE_MODEL = 'NanoESP32'

# Número de pines.
NUM_OF_PINS = 24

# Revisión.
SKETCH_REVISION = '1.0.0/150725'

# Credenciales de la red wifi.
SSID_NAME = 'DeviceControlSystem'
SSID_PASSWORD = 'lavidaesbella2018'

# IP asignada al servidor de comandos.
IP_SERVER = '0.0.0.0'

# Indicador wifi. Se enciende cuando se recibe un comando, y se apaga cuando se envía su respuesta.
wifiLed = Pin(48, Pin.OUT, value=0)

# Socket para la comunicación con DCS.
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

# Px =   0  1  2 3 4 5 6 7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
gpio = (-1,43,44,5,6,7,8,9,10,17,18,21,38,47, 1, 2, 3, 4,11,12,46, 0,45,-1,-1)

# Se indican los pines asignados al sensor DHT22.
DHT22TempPin = 23
DHT22HumiPin = 24

# Almacenan el valor de la temperatura y la humedad relativa del sensor DHT22.
DHT = dht.DHT22(Pin(13))
DHT22Temperature = 0
DHT22Humidity = 0

# Almacena 24 objetos Pin para ser programados en función del tipo de dispositivo.
px = []

# Almacena el tipo de pin que se va programando.
pType = []

# Almacenará hasta 4 objetos de la clase ADC para ser utilizados con los pines analógicos.
adc = [0,0,0,0]

# Almacenará los nombres de los dispositivos asociados a cada pin.
dName = []

# Se inicializan las variables y los pines.
for x in gpio:
    pType.append('F')
    dName.append('')
    if x == -1:
        px.append(0)
    elif x in [0,45,46]:
        px.append(Pin(x, Pin.OUT, value=1))
    else:
        px.append(Pin(x, Pin.IN))

# Indica si DCS ha enviado el comando 's:init'.
isDCSInit = False

# Número total de pines del tipo 'P' más 'S' definidos. No pueden ser más de 5 en total.
numOfPWMPins = 0

# Mensajes de error.
EXECUTED           = '0'
COMMAND_UNKNOWN    = '1! Command unknown.'
MODULE_NO_INIT     = '2! No-initialized module.'
PIN_BUSY           = '3! Pin busy.'
PIN_OUT_OF_RANGE   = '4! Pin out of range.'
SYNTAX_ERROR       = '5! Syntax error.'
VALUE_OUT_OF_RANGE = '6! Value out of range.'
WRONG_PIN_TYPE     = '7! Wrong pin type.'
WRONG_VALUE        = '8! Wrong value.'
FUNCTION_BUSY      = '9| Function busy.' # Ya hay definidos 5 pines entre pines del tipo 'P' y 'S'.


#---------------------------------------------------------------------------------------------------


def read_DHT22(dummy):
    """Obtiene la temperatura y la humedad del sensor DHT22 conectado al GPIO14."""
    global DHT22Temperature
    global DHT22Humidity
    DHT.measure()
    DHT22Temperature = DHT.temperature()
    DHT22Humidity = DHT.humidity()


def execute_command(command):
    """Selector de comandos."""
    if not isDCSInit: return MODULE_NO_INIT
    if command.startswith('s:'): return set_pin(command)
    if command.startswith('w:'): return write_pin(command)
    if command == 'r:all': return read_all_pins()
    if command == 'r:cfg': return read_config()
    return COMMAND_UNKNOWN

def set_pin(command):
    """Ejecuta el comando 's:pinID,type,data,devName'"""
    # Se obtienen los diferentes parámetros.
    if command.count(',') != 3:
        return SYNTAX_ERROR
    pinID, pinType, data, devName = command[2:].split(',')
    # Se comprueba el ID del pin.
    pinID = pinID.strip()
    if not pinID.isdigit():
        return WRONG_VALUE
    pinID = int(pinID)
    if pinID == 0 or pinID > NUM_OF_PINS:
        return PIN_OUT_OF_RANGE
    # Se comprueba el tipo del pin.
    pinType = pinType.strip()
    if len(pinType) != 1:
        return SYNTAX_ERROR
    if not pinType in 'OIAPS':
        return WRONG_PIN_TYPE
    # Se obtiene el valor de 'data'.
    data = data.strip()
    if len(data) == 0:
        return SYNTAX_ERROR
    # Se comprueba el nombre del dispositivo.
    if len(devName) < 3:
        return WRONG_VALUE
    # Se comprueba que el pin no esté ya ocupado.
    if pType[pinID] == pinType:
        if dName[pinID] != devName:
            return PIN_BUSY
        else:
            return EXECUTED + "Executed."
    
    # Tipo OUTPUT.
    if pinType == 'O':
        if len(data) != 1 or not data in '01':
            return WRONG_VALUE
        px[pinID].init(Pin.OUT, value=int(data))
        pType[pinID] = 'O'

    # Tipo INPUT.
    elif pinType == 'I':
        if pinID > 19:
            return PIN_OUT_OF_RANGE
        if len(data) != 1 or not data in '012':
            return WRONG_VALUE
        if data == '0':
            px[pinID].init(Pin.IN, pull=None)
        elif data == '1':
            px[pinID].init(Pin.IN, pull=Pin.PULL_UP)
        else:
            px[pinID].init(Pin.IN, pull=Pin.PULL_DOWN)
        pType[pinID] = 'I'

    # Tipo ANALOG.
    elif pinType == 'A':
        if pinID == DHT22TempPin or pinID == DHT22HumiPin:
            ...
        else:
            if pinID < 16 or pinID > 19:
                return PIN_OUT_OF_RANGE
            if data != '0':
                return SYNTAX_ERROR
            adc[pinID - 16] = ADC(px[pinID])
            adc[pinID - 16].atten(ADC.ATTN_11DB)
        pType[pinID] = 'A'

    # Tipo PWM.
    elif pinType == 'P':
        global numOfPWMPins
        if numOfPWMPins == 5:
            return FUNCTION_BUSY
        if len(data) > 4 or not data.isdigit() or int(data) > 1023:
            return WRONG_VALUE
        px[pinID] = PWM(Pin(gpio[pinID]), freq=1000, duty=int(data))
        pType[pinID] = 'P'
        numOfPWMPins += 1

    # Tipo SERVO.
    elif pinType == 'S':
        global numOfPWMPins
        if numOfPWMPins == 5:
            return FUNCTION_BUSY
        if len(data) > 3 or not data.isdigit() or int(data) < 51 or int(data) > 102:
            return WRONG_VALUE
        px[pinID] = PWM(Pin(gpio[pinID]), freq=50, duty=int(data))
        pType[pinID] = 'S'
        numOfPWMPins += 1

    else:
        return WRONG_PIN_TYPE
    
    dName[pinID] = devName
    return EXECUTED + "Executed."
        
    
def write_pin(command):
    """Ejecuta el comando 'w:pinID,value'"""
    # Se obtienen los diferentes parámetros.
    if command.count(',') != 1:
        return SYNTAX_ERROR
    pinID, value = command[2:].split(',')
    # Se comprueba el ID del pin.
    pinID = pinID.strip()
    if not pinID.isdigit():
        return WRONG_VALUE
    pinID = int(pinID)
    if pinID == 0 or pinID > NUM_OF_PINS:
        return PIN_OUT_OF_RANGE
    # Se comprueba el valor a establecer.
    value = value.strip()
    if len(value) == 0 or not value.isdigit():
        return WRONG_VALUE

    # Tipo OUTPUT.
    if pType[pinID] == 'O':
        if not value in '01':
            return VALUE_OUT_OF_RANGE
        px[pinID].value(int(value))

    # Tipo PWM.
    elif pType[pinID] == 'P':
        if len(value) > 4 or int(value) > 1023:
            return VALUE_OUT_OF_RANGE
        px[pinID].duty(int(value))

    # Tipo SERVO.
    elif pType[pinID] == 'S':
        if len(value) > 3 or int(value) < 51 or int(value) > 102:
            return VALUE_OUT_OF_RANGE
        px[pinID].duty(int(value))

    else:
        return WRONG_PIN_TYPE

    return EXECUTED + "Executed."


def read_all_pins():
    """Lee las muestras presentes en los pines."""
    response = '0X,'
    for pinID in range(1, NUM_OF_PINS + 1):
        if pType[pinID] == 'I':
            response += str(px[pinID].value()) + ','
        elif pType[pinID] == 'A':
            if pinID == DHT22TempPin:
                response += str(DHT22Temperature) + ','
            elif pinID == DHT22HumiPin:
                response += str(DHT22Humidity) + ','
            else:
                response += str(get_analog_sample(adc[pinID - 16])) + ','
        else:
            response += '-,'
    response = response[:-1]
    return response


def read_config():
    """Obtiene la programación actual de los pines."""
    response = EXECUTED
    for pinID in range(1, NUM_OF_PINS + 1):
        if pType[pinID] == 'F':
            continue
        response += 'P' + str(pinID) + \
                    '(GPIO' + str(gpio[pinID]) + '):' + \
                    pType[pinID] + ':' + \
                    dName[pinID] + '\n'
    if len(response) > 1:
        response = response.strip()
    else:
        response += 'No devices found.'
    return response
        

def get_analog_sample(adc):
    """Obtiene una muestra analógica."""
    maxValue = 0
    minValue = 3300
    totalSamples = 0
    for i in range(0, 22):
        sample = adc.read_uv() // 1000
        if sample > maxValue:
            maxValue = sample
        if sample < minValue:
            minValue = sample
        totalSamples += sample
    totalSamples -= (maxValue + minValue)
    return totalSamples // 20


#---------------------------------------------------------------------------------------------------


def wifi_connection():
    """Conecta con la red wifi."""
    wifiLed.on()
    wlan = network.WLAN()
    wlan.active(True)
    if not wlan.isconnected():
        print('Connecting to network (wait please).')
        wlan.connect(SSID_NAME, SSID_PASSWORD)
        while not wlan.isconnected():
            idle()
    wifiLed.off()
    global IP_SERVER
    IP_SERVER = wlan.ipconfig('addr4')[0]
    print('Module IP address:', IP_SERVER)


#---------------------------------------------------------------------------------------------------


def check_crc32(data):
    """Comprueba el CRC32 del comando recibid desde DCS. El formato es 'crc32.command'."""
    dotPos = data.find('.')
    if dotPos == -1:
        return False
    else:
        crc = data[0:dotPos]
        if crc32(data[dotPos + 1:]) == int(crc):
            return data[dotPos + 1:]
        else:
            return False


def start_UDP_server():
    """Inicia el servidor de comandos."""
    sockaddr = socket.getaddrinfo(IP_SERVER, 50000)[0][-1]
    sock.bind(sockaddr)
    print("UDP server up and listening.")


def get_command():
    """Obtiene el comando recibido desde DCS. Retorna la tupla (command, clientAddress)."""
    dataFromClient = sock.recvfrom(1024)
    wifiLed.on()
    data = dataFromClient[0].decode()
    if check_crc32(data):
        return (data[data.find('.') + 1:], dataFromClient[1])
    else:
        return ('! Communication error', '')

def send_response(response, clientAddress):
    """Envía la respuesta de la ejecución del comando al cliente."""
    dataToClient = str(crc32(response)).encode() + b'.' + response.encode()
    sock.sendto(dataToClient, clientAddress)
    wifiLed.off()


#---------------------------------------------------------------------------------------------------


def check_update():
    """Comprueba si hay una actualización del sketch."""
    response = requests.get('http://192.168.2.85/var/www/html/updates/NanoESP32/getNewRevision.php')
    newRevision = response.text
    if (newRevision != SKETCH_REVISION):
        return True
    else:
        return False


def start_dcm():
    """
    Programa principal.
    Se ejecuta continuamente a no ser que haya disponible una actualización. En este caso se retorna
    a 'main.py' para que se actualice y se reinicie.
    """
    wifi_connection()
    start_UDP_server()
    read_DHT22(0)  # Se invoca para actualizar las lecturas del sensor DHT22.

    # Lectura periódica del sensor DHT22.
    DHT22Timer = Timer(0)
    DHT22Timer.init(period=3000, mode=Timer.PERIODIC, callback=read_DHT22)

    # Se enciendo el led rojo durante un segundo para indicar el reinicio del módulo.
    px[20].value(0)
    sleep(1)
    px[20].value(1)

    while True:
        command, clientAddress = get_command()
        if command == 'update':
            if check_update():
                send_response('0Updating.', clientAddress)
                DHT22Timer.deinit()
                return
            else:
                send_response('0Nothing to update.', clientAddress)
                continue
        if command == 's:reset':
            send_response('0Ordered', clientAddress)
            reset()
        if command == 's:init':
            send_response(EXECUTED + MODULE_MODEL + '-' + str(NUM_OF_PINS) + '-' + SKETCH_REVISION, clientAddress)
            global isDCSInit; isDCSInit = True
            continue
        send_response(execute_command(command), clientAddress)
