ESP32 Horloge à LED

De knowledge
Aller à la navigation Aller à la recherche

Présentation

On se propose de réaliser une Horloge à LED.

Les besoins sont :

  • Récupérer l'heure (avec une connectivité Internet c'est facile) en NTP sur pool.ntp.org par exemple.
  • Afficher l'heure sur une matrice de LED basée sur un MAX7219.
  • Il faut aussi se localiser car un serveur NTP donne l'heure TU (avant on disait GMT) il nous faut connaitre la "time zone" (le fuseau horaire) et l'état DST (Heure d'été / heure d'Hiver). Des API existent pour cela, Abstract API en est une.
  • Comme cette API est implémentée en HTTPS il faudra également utilise la librairie https.
  • Toujours à cause de l'API Abstract il faudra savoir gérer du JSON

Montage et branchements

On se base sur la page déjà écrite pour brancher une carte "breakout" ESP32 V2 et un module à base de MAX7219.

ESP32+LEDMATRIX.png

Attention.png
ATTENTION: Je refais ma remarque faite dans la page correspondante. Alimenter les 256 LEDs consomme pas mal (256x20 mA) > 5 A! L'alim USB branchée sur le port USB ne peut pas suivre. Branché sur le port USB A de mon laptop... ça a du mal, en revanche avec un câble µUSB - USBC branché sur le "dock" ca marche bien.

Avec mon nouveau Samsung (2024) sur le port USB de droite ca marche même avec l'écran allumé. Clairement les nouvelles machines fournissent plus de courant.

Code

Code de test

On teste le montage avec le code suivant.

#include <MD_Parola.h>
#include <MD_MAX72xx.h>
#define HARDWARE_TYPE MD_MAX72XX::FC16_HW
#define MAX_DEVICES 4 // 4 blocks
#define CS_PIN 21

// create an instance of the MD_Parola class
MD_Parola ledMatrix = MD_Parola(HARDWARE_TYPE, CS_PIN, MAX_DEVICES);

void setup() {
  // Initialize the breakout buildin led
  pinMode(2,OUTPUT);
  digitalWrite(2,HIGH);
  ledMatrix.begin();         // initialize the LED Matrix
  ledMatrix.setIntensity(0); // set the brightness of the LED matrix display (from 0 to 15)
  ledMatrix.displayClear();  // clear LED matrix display
}

void loop() {
  ledMatrix.setTextAlignment(PA_CENTER);
  ledMatrix.print("12:30"); // display text
}

Résultat:

LED MATRIX 1230.png

Ca marche on a une horloge à l'heure… deux fois par jours.

Mise à l'heure

En se connectant à un serveur NTP on pourrait afficher l'heure UT.

#include <WiFi.h>
#include <WiFiMulti.h>
#include <MD_Parola.h>
#include <MD_MAX72xx.h>
#include "SSIDs.h"

#define HARDWARE_TYPE MD_MAX72XX::FC16_HW
#define MAX_DEVICES 4 // 4 blocks
#define CS_PIN 21

// create an instance of the MD_Parola class
MD_Parola ledMatrix = MD_Parola(HARDWARE_TYPE, CS_PIN, MAX_DEVICES);

// Time update
void setClock() {
  configTime(0, 0, "pool.ntp.org");

  Serial.println("Waiting for NTP time sync: ");
  time_t nowSecs = time(nullptr);
  while (nowSecs < 8 * 3600 * 2) {
    delay(500);
    Serial.print(F("."));
    yield();
    nowSecs = time(nullptr);
  }

  Serial.println();
  struct tm timeinfo;
  gmtime_r(&nowSecs, &timeinfo);
  Serial.print("Current time: ");
  Serial.println(asctime(&timeinfo));
}

// Manage Wifi
WiFiMulti WiFiMulti;

void setup() {
  Serial.begin(115200);

  // Initialize the breakout buildin led
  pinMode(2,OUTPUT);
  digitalWrite(2,HIGH);
  ledMatrix.begin();         // initialize the LED Matrix
  ledMatrix.setIntensity(0); // set the brightness of the LED matrix display (from 0 to 15)
  ledMatrix.displayClear();  // clear LED matrix display

    WiFi.mode(WIFI_STA);
  WiFiMulti.addAP(PRIMARY_SSID);
  #ifdef SECONDARY_SSID
    WiFiMulti.addAP(SECONDARY_SSID);
  #endif

  // wait for WiFi connection
  Serial.println("Serach WIFI");
  ledMatrix.setTextAlignment(PA_CENTER);
  ledMatrix.print("WiFi");
  Serial.print("Waiting for WiFi to connect...");
  while ((WiFiMulti.run() != WL_CONNECTED)) {
    delay (100);
  }
  ledMatrix.setTextAlignment(PA_CENTER);
  ledMatrix.print(WiFi.SSID());
  delay (1000);
  
  Serial.println(" connected");
  ledMatrix.setTextAlignment(PA_CENTER);
  ledMatrix.print("Time");
  setClock();  
}
void loop() {
  struct tm timeinfo;
  time_t nows = time(nullptr);
  gmtime_r(&nows, &timeinfo);
  Serial.print(asctime(&timeinfo));
  int h = timeinfo.tm_hour;
  int m =  timeinfo.tm_min;
  char timeBuffer[6];
  sprintf(timeBuffer,"%d:%02d", h, m);
  
  ledMatrix.setTextAlignment(PA_CENTER);
  ledMatrix.print(timeBuffer); // display time
  unsigned long old=m;
  // Wait for next minute
  while (old==m) {
    nows = time(nullptr);
    gmtime_r(&nows, &timeinfo);
    m =  timeinfo.tm_min;
  }
}

Le fichier "SSID.h" contient une ou deux macros définissant le SSID primaire et celui de backup.

#define PRIMARY_SSID   "MABOXP", "lsqkdjlqksjdlqskjdlksq"
#define SECONDARY_SSID "MABOXS", "pozaidlsqkjdlzajdlqksj"

Dans l'ordre l'afficheur nous indique

LED WiFi.png

La connexion wifi s'établit.

Une fois celle-ci établie, comme on utilise WiFiMulti on affiche le SSID qui a été choisit.

Puis on affiche :

LED Time.png

Et puis enfin l'heure (mise a jour toutes les secondes)

LED 15h54.png

Attention.png
On y crois mais ATTENTION c'est l'heure TU (GMT). L'heure à Greenwich en hivers!

Gestion des "time zones"

On va utiliser l'API Abstract qui, à partir de notre IP publique va nous donner notre position, notre timezone etc.

L'URL de base de cette API est : https://ipgeolocation.abstractapi.com/v1/

Pour avoir son certificat on suit la procédure décrite dans la page l'API Abstract. A l'heure ou j'écris ces lignes c'est : Amazon Root CA 1 et elle est valable jusqu'au : January 16, 2038... on a du temps. En revanche le certificat de ipgeolocation.abstractapi.com expire le mercredi 22 mai 2024. Rien n'assure qu'après cette date le nouveau certificat de renouvellement sera toujours signé par Amazon!

Quand on développe un objet connecté on doit, de préférence, utiliser une URL dont on maitrise le certificat. Surtout prendre garde à ce que le serveur gérant le FW Update ne soit pas derrière un certificat que l'on ne maitrise pas.

Le certificat CA de Amazon est le suivant:

-----BEGIN CERTIFICATE-----
MIIDQTCCAimgAwIBAgITBmyfz5m/jAo54vB4ikPmljZbyjANBgkqhkiG9w0BAQsF
ADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6
b24gUm9vdCBDQSAxMB4XDTE1MDUyNjAwMDAwMFoXDTM4MDExNzAwMDAwMFowOTEL
MAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv
b3QgQ0EgMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALJ4gHHKeNXj
ca9HgFB0fW7Y14h29Jlo91ghYPl0hAEvrAIthtOgQ3pOsqTQNroBvo3bSMgHFzZM
9O6II8c+6zf1tRn4SWiw3te5djgdYZ6k/oI2peVKVuRF4fn9tBb6dNqcmzU5L/qw
IFAGbHrQgLKm+a/sRxmPUDgH3KKHOVj4utWp+UhnMJbulHheb4mjUcAwhmahRWa6
VOujw5H5SNz/0egwLX0tdHA114gk957EWW67c4cX8jJGKLhD+rcdqsq08p8kDi1L
93FcXmn/6pUCyziKrlA4b9v7LWIbxcceVOF34GfID5yHI9Y/QCB/IIDEgEw+OyQm
jgSubJrIqg0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC
AYYwHQYDVR0OBBYEFIQYzIU07LwMlJQuCFmcx7IQTgoIMA0GCSqGSIb3DQEBCwUA
A4IBAQCY8jdaQZChGsV2USggNiMOruYou6r4lK5IpDB/G/wkjUu0yKGX9rbxenDI
U5PMCCjjmCXPI6T53iHTfIUJrU6adTrCC2qJeHZERxhlbI1Bjjt/msv0tadQ1wUs
N+gDS63pYaACbvXy8MWy7Vu33PqUXHeeE6V/Uq2V8viTO96LXFvKWlJbYK8U90vv
o/ufQJVtMVT8QtPHRh8jrdkPSHCa2XV4cdFyQzR1bldZwgJcJmApzyMZFo6IQ6XU
5MsI+yMRQ+hDKXJioaldXgjUkK642M4UwtBV8ob2xJNDd2ZhwLnoQdeXeGADbkpy
rqXRfboQnoZsG4q5WTP468SQvvG5
-----END CERTIFICATE-----

On en fait une chaine de caractère et on la mets dans le fichier CA-certificat.h

const char* rootCACertificate = \
"-----BEGIN CERTIFICATE-----\n" \
"MIIDQTCCAimgAwIBAgITBmyfz5m/jAo54vB4ikPmljZbyjANBgkqhkiG9w0BAQsF\n" \
"ADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6\n" \
"b24gUm9vdCBDQSAxMB4XDTE1MDUyNjAwMDAwMFoXDTM4MDExNzAwMDAwMFowOTEL\n" \
"MAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv\n" \
"b3QgQ0EgMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALJ4gHHKeNXj\n" \
"ca9HgFB0fW7Y14h29Jlo91ghYPl0hAEvrAIthtOgQ3pOsqTQNroBvo3bSMgHFzZM\n" \
"9O6II8c+6zf1tRn4SWiw3te5djgdYZ6k/oI2peVKVuRF4fn9tBb6dNqcmzU5L/qw\n" \
"IFAGbHrQgLKm+a/sRxmPUDgH3KKHOVj4utWp+UhnMJbulHheb4mjUcAwhmahRWa6\n" \
"VOujw5H5SNz/0egwLX0tdHA114gk957EWW67c4cX8jJGKLhD+rcdqsq08p8kDi1L\n" \
"93FcXmn/6pUCyziKrlA4b9v7LWIbxcceVOF34GfID5yHI9Y/QCB/IIDEgEw+OyQm\n" \
"jgSubJrIqg0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC\n" \
"AYYwHQYDVR0OBBYEFIQYzIU07LwMlJQuCFmcx7IQTgoIMA0GCSqGSIb3DQEBCwUA\n" \
"A4IBAQCY8jdaQZChGsV2USggNiMOruYou6r4lK5IpDB/G/wkjUu0yKGX9rbxenDI\n" \
"U5PMCCjjmCXPI6T53iHTfIUJrU6adTrCC2qJeHZERxhlbI1Bjjt/msv0tadQ1wUs\n" \
"N+gDS63pYaACbvXy8MWy7Vu33PqUXHeeE6V/Uq2V8viTO96LXFvKWlJbYK8U90vv\n" \
"o/ufQJVtMVT8QtPHRh8jrdkPSHCa2XV4cdFyQzR1bldZwgJcJmApzyMZFo6IQ6XU\n" \
"5MsI+yMRQ+hDKXJioaldXgjUkK642M4UwtBV8ob2xJNDd2ZhwLnoQdeXeGADbkpy\n" \
"rqXRfboQnoZsG4q5WTP468SQvvG5\n" \
"-----END CERTIFICATE-----\n";

On inclus ce fichier en tête de notre programme. On crée également un fichier Abstract.h qui contient l'API_KEY qui nous identifie associé à l'URL:

#define ABSTRACT_URL "https://ipgeolocation.abstractapi.com/v1/?api_key=xxxxxx&fields=timezone"

On ajoute en tête de programme :

#include <HTTPClient.h>
#include <WiFiClientSecure.h>
#include "Abstract.h"

Pour inclure les librairies http client et ssl (TLS) Et on ajoute une fonction getAbstractApiInfo():

String getAbstractApiInfo(){
  WiFiClientSecure *client = new WiFiClientSecure;
   if(client) {
     client -> setCACert(rootCACertificate);
       {
       HTTPClient https;
       Serial.print("[HTTPS] begin...\n");
       if (https.begin(*client, ABSTRACT_URL)) {  // Defined in Abstract.h
         Serial.print("[HTTPS] GET...\n");
         // start connection and send HTTP header
         int httpCode = https.GET();
         // httpCode will be negative on error
         if (httpCode > 0) {
           // HTTP header has been send and Server response header has been handled
           Serial.printf("[HTTPS] GET... code: %d\n", httpCode);
           // file found at server
           if (httpCode == HTTP_CODE_OK || httpCode == HTTP_CODE_MOVED_PERMANENTLY) {
             String payload = https.getString();
             return payload;
           }
          } else {
           Serial.printf("[HTTPS] GET... failed, error: %s\n", https.errorToString(httpCode).c_str());
          }
          https.end();
        } else {
          Serial.printf("[HTTPS] Unable to connect\n");
        }
      }
    }
  return ("ERROR");
}

Qui nous renvoie une chaine :

{"timezone":{"name":"Europe/Paris","abbreviation":"CET","gmt_offset":1,"current_time":"20:55:32","is_dst":false}}

Qui est du JSON :

{
    "timezone": {
        "name": "Europe/Paris",
        "abbreviation": "CET",
        "gmt_offset": 1,
        "current_time": "20:55:32",
        "is_dst": false
    }
}

Il nous faut alors utiliser la librairie standard JSON écrite pour les utilisateurs Arduino et qui est très légère : #include <Arduino_JSON.h>

Elle est compatible ESP32 et prendra donc peu de place.

On ajoute :

#include <Arduino_JSON.h>

Et on ajoute à la fin de setup():

  ledMatrix.setTextAlignment(PA_CENTER);
  ledMatrix.print("Geoloc");
  String loc=getAbstractApiInfo();

  JSONVar myObject = JSON.parse(loc);
  // JSON.typeof(jsonVar) can be used to get the type of the variable
  if (JSON.typeof(myObject) == "undefined") {
    Serial.println("Parsing input failed!");
    return; // exit form setup reboot
  }

  int hShift;
  if (myObject.hasOwnProperty("timezone")) {
    hShift=(int)myObject["timezone"]["gmt_offset"];
  }
  int sShift=hShift*3600;
  
  String timeZone="TU";
  if (sShift>=0)
    timeZone+="+";
  timeZone+=String(hShift);
  ledMatrix.print(timeZone);
  delay (5000);
  timeShift=sShift;

Les deux premières lignes ne sont là que pour afficher:

LED Matrix Geoloc.png

Pendant les requêtes de géolocalisation.

Ensuite on appelle l'API "Abstract" pour connaitre la time-zone de l'IP publique de l'ESP32.

On "parse" ce code JSON pour en retirer te GMT Offset (en heures). On le traduit en secondes car certaines parties du monde n'ont pas un décalage horaire en heures entières.

  • En Inde c'est UTC+5:30 (5.5)
  • En Australie centrale (Alice Spring, Adelaide...) c'est 9H1/2 en hiver et 10h en été (oui ils sont compliqués les Australiens).
  • Le Népal est a 5:45 ou 5 3/4 ou 5.75....

Donc faut gérer.

Ensuite on fait une jolie "string" avec TU + ou - le dévalage.

Et on l'affiche :

LED MATRIX UTC+1.png

On aurait la place de faire un GMT+1 ou un UTC+1 mais en cas de +5.75 ... faut prévoir. (même là le 5 de 75 va passer par dessus bord… tampi pour les Népalais j'ai pas un afficheur assez grand)

Remarque :
Attention.png
On pourrait afficher des messages plus longs en ajoutant un ou deux digits à la suite. Moi je n'ai trouvé que des cartes toutes faites de 4 digits mais elles semblent "sécables". Il suffit de les relier entres elles avec les 5 fils et de déclarer : #define MAX_DEVICES 6 au début. Peut être au Népal les boutiques de makers vendent des breakout de 6 digits!

Ensuite l'afficheur indique l'heure où que vous soyez (pourvu que qu'un WIFI wifi ayant un SSID et un password indiqué dans "SSIDs.h" soit disponible.

La mise à jour du firmware

Dans le repo github c'est fait mais je ne l'ai pas encore décrit ici.

La configuration du Wifi par le client

C'est en cours de dev

Le support en ABS 3D et Acrylique CNC

Maintenant que l'on a un joli logiciel et un prototype avec des câbles Dupont il faudrait réaliser un joli support. J'ai choisit de garder l'électronique apparente. Au niveau conformité CE ça va pas être fameux mais… on va pas le vendre.

Sur GitHub tout est dans le répertoire /hardware.

J'ai conçu un support qui s'imprime en ABS sans supports sur mon Ultimaker 2ex et une plaque de plastique acrylique de 3mm découpé à la CNC (ma 3018).

La modélisation a été faite avec FreeCad qui est un excellent produit!

LedClockViewFREECAD.pngHorlogeLedPhoto.png

A gauche la vue "Freecad" et à droite la photo de la Vraie horloge.

Pour la réalisation il faudra :

  • De l'ABS de la couleur de votre choix. (du PLA fera aussi l'affaire mais ne le poser pas derrière une fenêtre votre Horloge risque de "fondre au soleil")
  • Une plaque d'acrylique transparent (semi-transparent ça peut le faire… je vais essayer d'en trouver).
  • De la quincaillerie:
    • Deux Vis a tête cylindrique six pans creux M4×30 pour fixer la vitre (sur la photo il y a deux rondelles mais c'est une option)
    • 4 vis a tête bombée six pans creux M3×10
    • 4 vis a tête cylindrique six pans creux M2.5×14 et son écrou pour fixer le breakout ESP32
    • 4 inserts taraudés en laiton M2×4
    • 4 inserts taraudés en laiton M3x5.7
  • Une carte "breakout" ESP32 DevKit V2 (celle avec les support de broches jaunes et 2×19 broches et des trous de 2.5mm aux 4 coins)
  • Une carte à base de 4 MAX7219 et4 afficheurs à LED 1088AS

Doit y en avoir pour 30€ max.