Micropython: Reloj sincronizado por Internet

En Micropython tenemos distintas formas de medir el tiempo e incluso podemos tener un reloj que lleve la hora y la fecha, lo cual es sumamente útil en muchas aplicaciones. Podemos por ejemplo disparar eventos con una frecuencia determinada o realizar acciones en ciertos días de la semana. Sin embargo, estas funciones se basan en contadores internos que comienzan a funcionar cada vez que el micro se enciende y si éste no tiene una batería que lo mantenga funcionando, al apagarlo vuelven a cero. Aprovechando la capacidad de conectarse a Internet del ESP8266, en este artículo vamos a ver como construir un reloj que, cada vez que lo encendamos, se va a sincronizar tomando la hora y fecha correcta usando un servicio de Internet.

Introducción

En este proyecto vamos a mostrar los datos de fecha y hora empleando una placa Wemos D1 Mini con un display shield OLED para simplificar las conexiones, aunque el programa se puede adaptar a cualquier placa con ESP8266 (y ESP32). En un artículo anterior ya vimos como manejar este shield y las distintas funciones para escribir en el display y en otro artículo repasamos los distintos métodos que contiene el módulo time para hacer retardos y mediciones de tiempo. En este proyecto vamos a integrar ambos, así que los invito a revisarlos si aún no lo han hecho.

El protocolo NTP

Si hacemos alguna tarea en la que la información de la hora o la fecha es importante, es vital poder contar con alguna referencia fácilmente accesible que nos permita sincronizar nuestra información con alguna fuente confiable y que nos permita “poner en hora” nuestro reloj. En la actualidad, si disponemos de acceso a Internet esto se puede lograr con facilidad gracias al protocolo NTP (Network Time Protocol). El NTP es un protocolo creado en 1985 que se ha mantenido hasta nuestros días como el estandard para la sincronización horaria. Nos permite acceder a los denominados servidores NTP que son equipos distribuidos por todo el mundo capaces de suministrar la hora exacta, con una precisión entre 1 y 10 milisegundos.

Existen numerosos servidores NTP, algunos mantenidos por organismos oficiales en cada país que suministran información horaria oficial. Sin embargo, todos nos proveen la misma información, puesto que todos nos dan la hora en formato UTC. Algunos de estos servidores son los siguientes:

PaísServidor
Argentinantp.ign.gob.ar
Chilentp.shoa.cl
Españahora.roa.es
México cronos.cenam.mx
Internacionaltime.google.com
time.windows.com
time.apple.com
Algunos servidores NTP

La página de NTP Pool Project tiene listados una gran cantidad de servidores por zona geográfica.

Hora UTC

La hora o tiempo UTC (Tiempo Universal Coordinado) es un estándar de tiempo internacional. Reemplaza a la hora de Greenwich (o GMT). A partir de la hora UTC se definen zonas horarias (timezones) que suman o restan horas al UTC según la posición geográfica respecto al meridiano de Greenwich.

El módulo ntptime

Para acceder a un servidor NTP Micropython pone a nuestro servicio el módulo ntptime, que nos resuelve todos los detalles de la conexión y nos permite obtener los valores horarios de una forma muy sencilla. Lo primero que debemos hacer es importar el módulo:

import ntptime

Una vez hecho esto podemos emplear el método settime, que tiene el siguiente formato:

settime ()

El método settime conecta por omisión con el servidor pool.ntp.org y actualiza directamente los valores de tiempo local del ESP con la hora UTC. Si queremos cambiar el servidor, debemos realizar la asignación ntptime.localhost=servidor antes de llamar a settime.

settime no ofrece la posibilidad de ajustar la hora según nuestra zona horaria o timezone, así que deberemos hacer el ajuste en el programa luego de la sincronización.

Ejemplo:

import network
import ntptime
import time

def conectaWifi (red, password):
     global miRed
     miRed = network.WLAN(network.STA_IF)     
     if not miRed.isconnected():              #Si no está conectado…
         miRed.active(True)                   #activa la interface
         miRed.connect(red, password)         #Intenta conectar con la red
         print('Conectando a la red', red +"…")
         timeout = time.time ()
         while not miRed.isconnected():           #Mientras no se conecte..
             if (time.ticks_diff (time.time (), timeout) > 10):
                 return False
     return True

ntptime.host = "ntp.ign.gob.ar"

if conectaWifi ("red", "contraseña"):
     print ("Conexión exitosa!")
     print('Datos de la red (IP/netmask/gw/DNS):', miRed.ifconfig())

     print ("Hora inicial: ", time.localtime ()) 
     ntptime.settime () 
     print ("Hora sincronizada: ", time.localtime ())
else:
     print ("Imposible conectar")

Al correr el programa de ejemplo, podremos ver una salida como la siguiente:

Como se puede ver, la hora inicial corresponde al 1/1/2000 porque el ESP recién enciende y luego se actualizó a la del momento en que corrí el programa y se conectó con el servidor NTP.

Si al probarlo notan que la hora inicial ya es la hora y fecha actual, y esto persiste aunque reseteen o hasta apaguen la placa, no desesperen, no significa que repentinamente su placa adquirió la capacidad de almacenar estos datos aún estando apagada, es Thonny que desde la versión 3.3.0 actualiza la hora de la placa cada vez que se conecta.

Programa de ejemplo

Veamos ahora como queda la versión final del programa, con el LCD incluido:

import network, time, sys
import ntptime
from machine import I2C, Pin
from ssd1306 import SSD1306_I2C

def conectaWifi (red, password):
     global miRed
     miRed = network.WLAN(network.STA_IF)     
     if not miRed.isconnected():              #Si no está conectado…
         miRed.active(True)                   #activa la interface
         miRed.connect(red, password)         #Intenta conectar con la red
         print('Conectando a la red', red +"…")
         timeout = time.time ()
         while not miRed.isconnected():           #Mientras no se conecte..
             if (time.ticks_diff (time.time (), timeout) > 10):
                 return False
     return True

i2c = I2C(scl=Pin(5), sda=Pin(4), freq=100000)
lcd = SSD1306_I2C (64,48,i2c)

ntptime.host = "ntp.ign.gob.ar"
timezone = -3
lcd.fill (0)
lcd.text ("Wifi…",0,0,1)
lcd.show ()

if conectaWifi ("red", "contraseña"):
     print ("Conexión exitosa!")
     print('Datos de la red (IP/netmask/gw/DNS):', miRed.ifconfig())

    #Indicar conexion exitosa
    lcd.fill (0)
    lcd.text ("Wifi OK",0,0,1) 
    lcd.show () 
    time.sleep (1) 
    lcd.fill (0) 

    try:
        #Sincroniza fecha y hora     
        ntptime.settime ()

    except:
        #Error     
        print ("Error NTP!")
        
        lcd.fill (0)
        lcd.text ("NTP ERR",0,0,1)
        lcd.text ("RESET",0,10,1)
        lcd.show ()
        sys.exit () 

    #Desconecta el Wifi para ahorrar energía
    miRed.active (False) 
   
    while (True):
       
       #Muestra hora y fecha en el display     
       hora_local_seg = time.time () + timezone * 3600  #Hora local en segundos     
       hora_local = time.localtime (hora_local_seg)     #Pasa a tupla de 8 elementos     
    
       #Imprimir hora_local en LCD
       lcd.fill (0)     #Imprime la hora     
       lcd.text ("{:02}".format (hora_local[3]),10,10,1)     
       lcd.text (":", 25,10,1)     
       lcd.text ("{:02}".format (hora_local[4]),32,10,1)     
       
       #Imprime la fecha     
       lcd.text ("{:02}".format (hora_local[2]),10,20,1)     
       lcd.text ("/", 25,20,1)     
       lcd.text ("{:02}".format (hora_local[1]),32,20,1)     
       lcd.show()
 
else:
     print ("Imposible conectar")
     lcd.fill (0) 
     lcd.text ("Wifi ERR",0,0,1) 
     lcd.text ("RESET",0,10,1) 
     lcd.show () 
     sys.exit ()

Analicemos un poco el código, que se hizo extenso:

De la línea 1 a la 4 se importan todos los módulos que se van a emplear luego.

Entre las líneas 6 a 17 se encuentra la definición de la función conectaWifi, para conectarse a una red.

En las líneas 19 y 20 se crean los objetos del bus I2C y el LCD, para poder usar el display.

En la línea 22 se define el servidor NTP. Pueden cambiar este valor o simplemente no poner nada y dejar que se use el servidor por omisión.

En la línea 23 defino la variable timezone con el valor -3, porque estoy en Argentina y mi zona horaria es UTC-3. Ustedes deben verificar su zona horaria y cambiar este valor. Pueden ver la zona horaria que les corresponde en esta página.

Entre la línea 24 a 26 se imprime el mensaje Wifi… indicando que está intentando realizar una conexión con la red Wifi.

En la línea 28 el programa intenta conectarse a la red. Si tiene éxito imprime los datos de la red (línea 30) y luego imprime el mensaje Wifi OK en el LCD (líneas 32 a 37).

Lo que sigue es una estructura interesante, que voy a copiar para una explicación mas detallada:

try:
    #Sincroniza fecha y hora
    ntptime.settime ()

except:
    #Error
    print ("Error NTP!")     
    lcd.fill (0)
    lcd.text ("NTP ERR",0,0,1)
    lcd.text ("RESET",0,10,1)
    lcd.show ()
    sys.exit ()

Siempre que escribamos un programa debemos tener en cuenta que algo puede funcionar mal. En el caso de la función conectaWifi por ejemplo, se usa un timeout de 10 segundos y si en ese tiempo no se logró la conexión, se sale de la función con un código de error.

El método ntptime.settime puede producir un error debido a varias razones, como que el servidor no responda o tarde mucho en responder. El problema es que no devuelve un código de error que podamos manejar fácilmente sino que produce lo que se llama Runtime error o error en tiempo de ejecución, que detiene el programa, lo cual no es conveniente. Para controlar estos casos, Micropython deriva de Python lo que se denomina control de excepciones.

El control de excepciones se logra con una estructura try except . Luego de try debemos escribir el código que puede generar un error (en este caso, la conexión al servidor NTP) y luego de except las instrucciones a ejecutar si se produce el error. En este caso particular, que es sencillo, si se produce un error al querer conectar, no tiene sentido seguir porque no se puede actualizar la hora, así que se muestra un mensaje de error y el programa finaliza.

Siguiendo con el código, en la línea 54 se desconecta la red Wifi para ahorrar energía, ya que no la usamos mas (es opcional).

En la línea 56 se inicia un bucle sin fin en donde se muestra permanentemente la hora y la fecha en el LCD.

En la línea 59 se carga en la variable hora_local_seg la información de fecha y hora que devuelve el método time, que representa la cantidad de segundos desde el 1/1/2000 mas (o menos, según el signo) la cantidad de segundos que represente la variable timezone. De esta manera, hora_local_seg tiene la información en nuestro huso horario (pero en segundos).

La línea 60 pasa ese valor en segundos desde 1/1/2000 a una tupla de 8 elementos, tal como vimos que hace el método localtime, para poder manejar los valores de horas, minutos, día, etc. Es importante notar que no podemos simplemente sumar o restar el ajuste de timezone al campo de la hora, porque si lo hacemos así, el resultado no se propagará a los otros campos. Por ejemplo si la hora es 23 UTC y le sumamos 2, no solo importa que la hora ajustada ahora es la 1, sino que ese ajuste nos lleve al día siguiente (y tal vez al mes siguiente y año siguiente).

Desde la línea 62 a la 72 se muestran en el LCD los valores de hora, minutos, día y mes. Es importante observar que el reloj interno de la placa mantiene la hora UTC, no ajustada porque este valor no se puede escribir. Por eso cada vez que lo necesitamos para mostrar al LCD debemos leerlo y ajustarlo con el timezone. Pueden consultar los especificadores de formato empleados en esta página.

Finalmente la línea 74 retoma el if de la línea 28 y muestra un mensaje de error si no se pudo lograr la conexión con la red Wifi, terminando también el programa.

Como paso final vamos a descargar el código en la placa, para que funcione de manera autónoma, sin depender de Thonny. Para ello debemos guardar el archivo en la placa y no en nuestra computadora. Existen dos nombres de archivo con significado especial:

boot.py: Contiene el código que se ejecuta cuando la placa arranca desde un reset y es necesario hacer alguna configuración. Al terminar pasa a main.py

main.py: Contiene el programa principal.

Grabaremos entonces nuestro programa del reloj con el nombre de main.py desde la opción Fichero Guardar Como de Thonny. Hecho esto podemos cerrar Thonny o alimentar nuestra placa con otra fuente para comprobar el funcionamiento del programa.

Exactitud

El método empleado en las placas ESP para llevar la información horaria depende de muchos factores (la temperatura por ejemplo) que pueden influir sobre su exactitud y hacer que tengan una deriva aún en períodos cortos de tiempo. Si se necesitan valores exactos deberá sincronizarse usado NTP con cierta frecuencia o directamente usar un chip RTC externo con batería

Conclusiones

Espero que el artículo les haya sido de utilidad, en próximas entregas seguiremos viendo mas aplicaciones de Micropython. Cualquier duda o consulta la pueden hacer en la sección de comentarios mas abajo.

Deja un comentario

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.

Habilitar notificaciones OK No, gracias