Internet de les coses amb ESP32 i ESP8266

Exemples Referència Plaques   Recursos CITCEA
Projectes Programació Perifèrics   Inici

Visualitzador de dades o avisos

Programa del grup 1

Aquest grup va fer un visualitzador de condicions meteorològiques per a la pràctica del surf. Amb aquest dispositiu, l'usuari selecciona una regió i la pantalla li mostra els millors llocs per anar a practicar el surf i li indica les dades de les onades, la temperatura i la velocitat del vent.

Pantalla del dispositiu

Les dades s'extreuen de l'API Open-Meteo i són processades per un programa en Python en un servidor remot hostatjat a la plataforma Render, d'aquesta manera el microcontrolador rep només el subconjunt de dades que interessen per donar resposta a la consulta de l'usuari.

El programa del microcontrolador és el següent:

#include <SPI.h>  // Carreguem la biblioteca SPI
#include <WiFiNINA.h> // Carreguem la biblioteca WiFiNINA
#include <Wire.h> // Llibreria per la comunicació I2C amb la pantalla (substitueix l'antic //nosabem)
#include "ssd1306.h" // libreria pantalla
// #include "ssd1306.h" // es labiblioteca de la pantalla que debemos usar
#include <Adafruit_NeoPixel.h> // Carreguem la biblioteca pel LED NeoPixel
// Configuració dels pins
#define BTN_IZQ 7 // Pin del botó esquerre     
#define BTN_RESET 5 // Pin del botó central   
#define BTN_DER 4 // Pin del botó de la dreta   
#define PIN_BUZZER 3   // Pin del brunzidor
#define PIN_NEOPIXEL 1 // Pin del LED NeoPixel
#define N_Mi5   1319 // Freqüència de la nota Mi
#define N_Sol5  1568 // Freqüència de la nota Sol (Afegida de la versió op1)
#define N_Do6   2093 // Freqüencia de la nota Do
// --- CONFIGURACIÓN DE RED ---
const char idXarxa[] = "iPhone de joan"; // Nom del punt d'accés 
const char contrasenya[] = "joanjoan";    // Contrasenya de connexió 
// IP del ordinador on corre el programa de python
char server[] = "servidor-visualitzador-de-missatges.onrender.com";
int port = 443;  // Port de comunicació
int status = WL_IDLE_STATUS;
WiFiSSLClient client;
// Inicialització del NeoPixel
Adafruit_NeoPixel semaforo = Adafruit_NeoPixel(1, PIN_NEOPIXEL, NEO_GRB + NEO_KHZ800);
uint16_t animHue = 0; // Variable per l'animació de l'arc de Sant Martí
int estadoApp = 0; // Variable que controla en quina pantalla está en cada moment
// 0: Menú de la pantalla
// 1: L'Arduino s'està conectant al Wi-Fi i descarregant dades
// 2: L'Arduino mostra a la pantalla el carrusel de les condicions meteorològiques
// Nombre total de regions disponibles a la pantalla (substitueix l'antic //CUIDADO CON ESTO...)
const int numComunidades = 7; // Total de regions disponibles a la pantalla
String comunidadesMenu[numComunidades] = {"Andalusia", "Asturies", "Canaries", "Cantabria", "Catalunya",
    "Galicia", "Pais_Basc"}; // noms per la comunicació amb python (sense espais en blanc)
String comunidadesBonitas[numComunidades] = {"Andalusia", "Asturies", "Canaries", "Cantabria", "Catalunya",
    "Galicia", "Pais Basc"}; // noms per la pantalla 
// Variable que indica on està situat el cursor en la llista del menú (posició en la llista)
int cursorMenu = 0; 
// Variable que serveix per lliscar la llista del menú cap avall quan el cursor arriba al final de la pantalla
int offsetMenu = 0; 
// Mapa de bits de l'onada
const unsigned char onada_bitmap[] PROGMEM = {
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80,
  0x80, 0x80, 0x80, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0,
  0x60, 0x30, 0x18, 0x8C, 0xC6, 0x63, 0x31, 0x99, 0x4C, 0x26, 0x92, 0x4B, 0x29, 0x35, 0x14, 0x14, 0x14,
  0x14, 0x10, 0x14, 0x31, 0x23, 0x66, 0x5C, 0xF0, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0xC0, 0xC0, 0xE0, 0xB8, 0x8C, 0x87, 0xC1, 0xF0,
  0x1C, 0x07, 0x01, 0x00, 0xF8, 0x0F, 0x01, 0x00, 0xFF, 0x81, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x80, 0x80, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02,
  0x02, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03,
  0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03,
  0x03, 0x03, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};
// Lloc on guardar i organitzar les dades que ens proporciona el fitxer de python 
struct Spot {
  String nombre; // Nom de la platja
  int numHoras; // Nombre d'hores de previsió guardades
  // lloc amb 12 espais per les guardar les dates
  // (perque cada espai guarda la informació de cada hora, fins a 12h)
  String fechas[12]; 
  String horas[12]; // ''
  String olas[12]; // ''
  String periodos[12]; // ''
  String temps[12];// ''
  String viento[12];// ''
  String dir_viento[12];// ''
  String dir_ola[12];// ''
  String tipo_viento[12];// ''
  String score[12]; // ''
};
Spot topSpots[5]; // crea les 5 platjes per guardar la informació de les 5 millors 
// variable per guardar quantes platjes ha enviat realment l'script de python. (de 0 a 5) 
int totalSpotsCargados = 0; 
int spotActual = 0; // Variable que indica quina platja s'estàn visualitzant les condicions meteorològiques        
int horaActualMostrada = 0; // Variable que indica l'hora de les 24h possibles (numero entre 0 i 23)
unsigned long ultimoCambio = 0; // Guarda el moment exacte en que va canviar l'hora en la pantalla
void setup() {
  Serial.begin(9600);
  pinMode(BTN_IZQ, INPUT_PULLUP);
  pinMode(BTN_RESET, INPUT_PULLUP);
  pinMode(BTN_DER, INPUT_PULLUP);
  pinMode(PIN_BUZZER, OUTPUT);
  // --- NEOPIXEL ---
  semaforo.begin(); 
  semaforo.setBrightness(30); 
  semaforo.setPixelColor(0, 0, 0, 0); 
  semaforo.show();
  // --- PANTALLA ---
  ssd1306_128x64_i2c_init();
  ssd1306_clearScreen();
  ssd1306_fillScreen(0);
  ssd1306_setFixedFont(ssd1306xled_font6x8);
  ssd1306_print("Conectant WiFi...");
  // WI-FI
  while (WiFi.status() != WL_CONNECTED) {
    Serial.print("Connectant a la xarxa ");
    Serial.println(idXarxa);
    for(int i = 0; i < 150; i++) { 
      animacionArcoiris(); 
    }
    status = WiFi.begin(idXarxa, contrasenya);
  }
  semaforo.setPixelColor(0, 0, 255, 0); semaforo.show(); delay(500);
  semaforo.setPixelColor(0, 0, 0, 0); semaforo.show(); delay(500);
  Serial.println("WiFi conectat");
  dibujarMenu(); // aplica la funció "dibujarMenu"
}
void loop() {
  // ESTAT 0: MENÚ
  if (estadoApp == 0) { // Si ens trobem en el menu (la llista de comunitats)
    if (digitalRead(BTN_DER) == LOW) { // si s'activa el botó dret
      tone(PIN_BUZZER, N_Mi5, 20); // fa sonar el brunzidor la nota Mi 20 milisegons
      cursorMenu++; // Suma 1 a la posició del cursor (baixa la posició del cursor a la seguent platja)
      // Si el cursor es pasa de l'última posició torna al principi de la llista
      if (cursorMenu >= numComunidades) cursorMenu = 0; 
      // Si el cursor baixa més enllá de la 5a línia visible, cambia de
      // pantalla mostrant les dues comunitats que falten per mostrar
      if(cursorMenu >= offsetMenu + 5) offsetMenu++; 
      // Si el cursor queda per sobre del que es visible ajusta l'inici de la llista
      // perquè el cursor sigui visible.
      if(cursorMenu < offsetMenu) offsetMenu = cursorMenu; 
      dibujarMenu(); // aplica la funció "dibujarMenu"
      delay(200);
    }
    if (digitalRead(BTN_IZQ) == LOW) { // Si s'activa el botó esquerre
      tone(PIN_BUZZER, N_Mi5, 20); // fa sonar el brunzidor la nota Mi 20 milisegons
      cursorMenu--; // Resta 1 a la posició del cursor (baixa la posició del cursor a la seguent platja)
      // Si el cursor es pasa de l'última posició torna al final de la llista
      if (cursorMenu < 0) cursorMenu = numComunidades - 1; 
      if(cursorMenu < offsetMenu) offsetMenu = cursorMenu;
      if(cursorMenu >= offsetMenu + 5) offsetMenu = cursorMenu - 4;
      dibujarMenu();
      delay(200);
    }
    if (digitalRead(BTN_RESET) == LOW) { // Si s'activa el botó central
      // Canvia l'estat de la pantalla a comunicar-se amb el python
      estadoApp = 1; 
      // 1. Dibuixem la pantalla IMMEDIATAMENT per no tenir retard visual
      ssd1306_clearScreen();
      ssd1306_setCursor(0, 10);
      ssd1306_print("Buscant onades...");
      ssd1306_setCursor(0, 22);
      ssd1306_print(comunidadesBonitas[cursorMenu].c_str());
      // Dibuixem l'onada estàtica a la PÀGINA 4 (Y=4). 
      // Com la llibreria funciona per pàgines (de 0 a 7),
      // Y=4 la situa perfectament a la meitat inferior (blau).
      ssd1306_drawBitmap(39, 4, 50, 32, onada_bitmap);
      // 2. Reproduïm la melodia (so) un cop la pantalla ja ha canviat
      tone(PIN_BUZZER, N_Mi5, 60); delay(80); // fa sonar el brunzidor la nota Mi
      tone(PIN_BUZZER, N_Sol5, 60); delay(80); 
      tone(PIN_BUZZER, N_Do6, 120); // fa sonar el brunzidor la nota Do
      delay(100);
    }
  }
  // ESTAT 1: l'Ardino passa a demanar-li dades a l'ordinador on está corrent l'script de python
  // per obtenir les dades necessaries d'internet
  else if (estadoApp == 1) { // Si l'estat és igual a 1 (l'Arduino passa a ser client)
    // fa la funció "peridrDatosAlOrdenador" (l'Arduino es comunica amb el python per demanar-li
    // les dades corresponents)
    pedirDatosAlOrdenador(); 
    // Si "totalSpotsCargados" és major que 0 significa que python ha trobat platjes i l'Arduino
    // les ha rebut correctament
    if (totalSpotsCargados > 0) { 
      estadoApp = 2; // Canvia l'estat de la pantalla a mostrar les 5 millors platjes
      // guarda el temps actual per saber, més endavant, quan començar a passar les pàgines de les 5 platjes
      ultimoCambio = millis(); 
      // fa la funció "pintarPantalla" (escriu la informació rebuda de la primera platja trobada)
      pintarPantalla(); 
    } else {
      // Si alguna cosa ha fallat, és a dir, l'script de python no ha trobat cap platja, torna al
      // menú principal on hi apareixen les comunitats
      estadoApp = 0; 
      dibujarMenu(); // Torna a escriure en la pantalla la llista de comunitats
    }
  }
  // ESTAT 2: CARRUSEL 
  else if (estadoApp == 2) {
    if (digitalRead(BTN_DER) == LOW) { // si s'activa el botó dret
      tone(PIN_BUZZER, N_Mi5, 20); 
      // Si s'activa el polsador de la dreta canvia a la seguent platja de les 5 millors
      // de la comunitat escollida
      spotActual++; 
      // Si s'arriba a l'última platja de la llista torna a la primera platja
      if (spotActual >= totalSpotsCargados) spotActual = 0; 
      horaActualMostrada = 0; 
      pintarPantalla();
      delay(200);
    }
    if (digitalRead(BTN_IZQ) == LOW) { // si s'activa el botó esquerre
      tone(PIN_BUZZER, N_Mi5, 20); 
      // Si s'activa el polsador de l'esquerra torna a l'anterior platja de les 5 millors
      // de la comunitat escollida
      spotActual--; 
      if (spotActual < 0) spotActual = totalSpotsCargados - 1; 
      horaActualMostrada = 0; 
      pintarPantalla();
      delay(200);
    }
    // Lògica de pulsació curta / llarga 
    if (digitalRead(BTN_RESET) == LOW) { // Si es torna a activar el polsador central torna al menú
      unsigned long tiempoPulsado = millis();
      while(digitalRead(BTN_RESET) == LOW) {} 
      if (millis() - tiempoPulsado > 800) {
        // Pulsació Llarga
        tone(PIN_BUZZER, N_Mi5, 100);
        estadoApp = 0; // Torna al menú
        semaforo.setPixelColor(0, 0, 0, 0); 
        semaforo.show();
        dibujarMenu(); // Torna a escriure en la pantalla la llista de comunitats
        delay(300);
      } else {
        // Pulsació Curta: TORNAR A L'HORA ACTUAL (0)
        tone(PIN_BUZZER, N_Do6, 50);
        horaActualMostrada = 0; 
        pintarPantalla();
        ultimoCambio = millis(); 
        delay(300);
      }
    }
    // Cada 3 segons canvia l'hora i les noves dades segons l'hora mostrada
    if (millis() - ultimoCambio > 3000) { 
      horaActualMostrada++;
      if (horaActualMostrada >= topSpots[spotActual].numHoras) {
        // Si l'hora que es mostra és igual o major que el total d'horas
        // possibles a mostrar (24h), torna a mostrar les dades de la
        // hora 0.0h (la primera hora)
        horaActualMostrada = 0; // reinicia el cicle horari
      }
      pintarPantalla();
      ultimoCambio = millis(); // Reinicia el cronòmetre per tornar a començar el cicle
    }
  }
}
// FUNCIONS
// PANTALLA MENU
void dibujarMenu() {
  ssd1306_clearScreen(); // esborra la pantalla
  ssd1306_setCursor(0, 0);
  ssd1306_print("On anem a fer surf?");
  for (int i = 0; i < 5; i++) { // bucle de 5 linias (les que caben a la pantalla)
    // Si el "idx" te valor 0 es dibuixa a la pantalla les 5 primeres comunitats,
    // i per exemple si el idx=2 dibuixa les comunitats de la 2 a la 6.
    int idx = offsetMenu + i; 
    // Si el càlcul de l'idx és més gran que el numero de comunitats (que és 7) es para el bucle
    if (idx >= numComunidades) break; 
    ssd1306_setCursor(0, 18 + (i * 9)); // Càlcul de la posició de cada linia
    // Si el idx és el mateix que el que es selecciona amb els botons, dibuixa a la pantalla ">" (el cursor)
    if (idx == cursorMenu) ssd1306_print("> "); 
    else ssd1306_print("  "); // Si no és la seleccionada pel cursor, posa dos espais buits
    ssd1306_print(comunidadesBonitas[idx].c_str()); // Escriu el nom de la comunitat a la pantalla
  }
}
// ANIMACIÓ NEOPIXEL
void animacionArcoiris() {
  animHue += 250; 
  uint32_t colorRGB = semaforo.ColorHSV(animHue, 255, 255);
  semaforo.setPixelColor(0, semaforo.gamma32(colorRGB));
  semaforo.show(); 
  delay(10); 
}
// SEMÀFOR PER SCORE (Ara amb ToFloat per guardar els decimals)
void actualizarSemaforo() {
  float scoreVal = topSpots[spotActual].score[horaActualMostrada].toFloat(); 
  if (scoreVal >= 0.65) { 
    semaforo.setPixelColor(0, 0, 0, 255); // Blau
  }
  else if (scoreVal >= 0.35 && scoreVal < 0.65) { 
    semaforo.setPixelColor(0, 0, 255, 0); // Verd
  }
  else if (scoreVal >= 0.1 && scoreVal < 0.35) { 
    semaforo.setPixelColor(0, 255, 150, 0); // Taronja
  } 
  else { 
    semaforo.setPixelColor(0, 255, 0, 0); // Vermell
  }
  semaforo.show(); 
}
// FUNCIO D'ERROR
void mostrarError(String mensaje) {
  ssd1306_clearScreen();
  ssd1306_setCursor(0, 20);
  ssd1306_print("ERROR:");
  ssd1306_setCursor(0, 35);
  ssd1306_print(mensaje.c_str());
  semaforo.setPixelColor(0, 255, 0, 0); 
  semaforo.show();
}
// DADES A LA PANTALLA
// Funció per agafar les dades que python ha enviat en format de text llarg (una versió d'un CSV)
// que l'Arduino ha guardat en "carpetes" 
// agafa de la carpeta que es vol extreure les dades i les escriu a la pantalla
void pintarPantalla() { 
  ssd1306_clearScreen(); // Esborra tot el que hi havia a la pantalla
  ssd1306_setCursor(0, 0);
  ssd1306_print(topSpots[spotActual].nombre.c_str()); // escriu la platja que s'ha escollit amb els botons
  ssd1306_setCursor(0, 9);
  // busca en la llista de dates la que correspon i l'escriu
  ssd1306_print(topSpots[spotActual].fechas[horaActualMostrada].c_str()); 
  ssd1306_print(" ");
  // busca en la llista de hores la que correspon i l'escriu
  ssd1306_print(topSpots[spotActual].horas[horaActualMostrada].c_str()); 
  ssd1306_setCursor(0, 19);
  ssd1306_print("Alcada: ");
  // busca en la llista de olas la correspon i l'escriu
  ssd1306_print(topSpots[spotActual].olas[horaActualMostrada].c_str()); 
  ssd1306_print(" m");
  ssd1306_setCursor(0, 27);
  ssd1306_print("Periode: ");
  // busca en la llista de periodes el que correspon i l'escriu
  ssd1306_print(topSpots[spotActual].periodos[horaActualMostrada].c_str()); 
  ssd1306_print(" s");
  ssd1306_setCursor(0, 35);
  ssd1306_print("Temp: ");
  // busca en la llista de temperatures el que correspon i l'escriu
  ssd1306_print(topSpots[spotActual].temps[horaActualMostrada].c_str()); 
  ssd1306_print(" oC");
  ssd1306_setCursor(0, 43);
  ssd1306_print("Vent: ");
  // busca en la llista de vents el que correspon i l'escriu
  ssd1306_print(topSpots[spotActual].viento[horaActualMostrada].c_str()); 
  ssd1306_print(" km/h");
  ssd1306_setCursor(0, 52);
  ssd1306_print("Tipus: "); // Versió dient " Tipus: "
  // busca en la llista de tipus de vent el que correspon i l'escriu
  ssd1306_print(topSpots[spotActual].tipo_viento[horaActualMostrada].c_str()); 
  actualizarSemaforo(); 
}
// COMUNICACIÓ ARDUINO (client) i PYTHON (servidor)
// Funció per comunicar l'Arduino i el servidor i procesar les dades del sistema.
void pedirDatosAlOrdenador() { 
  // L'Arduino obre una comunicació amb la IP proporcionada pel programa de python i el port
  if (client.connect(server, port)) { 
    client.setTimeout(5000); 
    // S'estableix la petició web HTTP (l'Arduino actua com a client HTTP)
    client.print("GET /?regio="); // l'Arduino vol obtenir informació
    // Li diem al servidor quina es la comunitat de la que es vol extreure les dades
    client.print(comunidadesMenu[cursorMenu]); 
    // Protocol HTTP
    // CANVI: Usem HTTP/1.0 en lloc de 1.1 per rebre text net sense blocs (chunks)
    client.println(" HTTP/1.0"); 
    // versió del protocol
    client.print("Host: "); // CANVI: Usem print en comptes de println perquè l'IP vagi a la mateixa línia
    // Indiquem que es un servidor local
    client.println(server);
    client.println("Connection: close");
    client.println();
    // Python rep p.ex. aixó: "GET /?regio=Galicia HTTP/1.0"
    // Com python triga una estona en buscar les dades a internet, posem un bucle d'una espera de
    // 40 segons abans de dir que hi ha hagut un error.
    unsigned long timeout = millis();
    while (client.available() == 0) {
      animacionArcoiris(); // L'onada ja no es mou aquí, només fem brillar el LED
      if (millis() - timeout > 40000) { 
        mostrarError("Pc calculant...");
        client.stop(); return; 
      }
    }
    semaforo.setPixelColor(0, 0, 0, 0); 
    semaforo.show();
    // Sistema per no guardar les capceleres tècniques tipus: "HTTP/1.0 200 OK" i
    // només tenir en compte les daddes de les platjes
    while (client.connected() || client.available()) { 
      String line = client.readStringUntil('\n');
      line.trim(); 
      if (line.length() == 0) break; 
    }
    // Guardem la informació que ens proporciona el python
    totalSpotsCargados = 0; // Reiniciem el contador de platjes perquè comenci per la 1a
    // Variable per indexar en quin lloc es vol guardar les dades que ens proporciona el python
    int index = 0; 
    // CANVI: Bucle de seguretat per saltar salts de línia en blanc abans d'agafar la primera platja
    String primeraLinea = "";
    while (client.connected() || client.available()) {
        primeraLinea = client.readStringUntil('\n'); 
        primeraLinea.trim();
        if (primeraLinea.length() > 0) break;
    }
    topSpots[index].nombre = primeraLinea; // Assignem el primer nom net
    topSpots[index].numHoras = 0; // establim el contador de hores a 0
    unsigned long tiempoUltimoDato = millis();
    // Procesem el text que envia python i l'organitzem en l'Arduino
    while (client.connected() || client.available()) { // Procesament de les dades de les platjes
      if (client.available()) {
        String linea = client.readStringUntil('\n');
        linea.trim();
        if (linea == "---") { // Quan l'Arduino llegeix "---" les dades de la platja actual han finalitzat
          index++; // passa a la seguent platja
          if (index >= 5) break; // Si ja ha processat les 5 platjes, para la lectura.
          topSpots[index].nombre = client.readStringUntil('\n'); // Guardem el nom de la seguent platja
          // Esborrem els caràcters invisibles (salts de línia, etc) per seguretat
          // (substitueix l'antic opcional)
          topSpots[index].nombre.trim(); 
          topSpots[index].numHoras = 0; // Reiniciem el contador d'hores a 0 
        } 
        // Tenim text separat per comes, utilitzem indexOf i substring per separar les dades
        // (substitueix l'antic //CANVIARHO PER UN SPLIT)
        else if (linea.length() > 0) { // localitzem la posició de les comes per desprès separar les dades
          String datos[10];
          int lastIndex = 0;
          for (int i = 0; i < 10; i++) { // separem les comes del fitxer
            int idx = linea.indexOf(',', lastIndex);
            if (idx == -1) {
              datos[i] = linea.substring(lastIndex);
              break;
            }
            datos[i] = linea.substring(lastIndex, idx);
            lastIndex = idx + 1;
          }
          // variable que representa l'hora actual de les dades que estem guardant per una platja
          int h = topSpots[index].numHoras; 
          if (h < 12) { // agafem el text proporcionat i el guardem en els seus llocs corresponents
            topSpots[index].fechas[h] = datos[0]; // demanem la primera dada (data)
            topSpots[index].horas[h] = datos[1]; // ''
            topSpots[index].olas[h] = datos[2]; // ''
            topSpots[index].periodos[h] = datos[3]; // ''
            topSpots[index].temps[h] = datos[4]; // ''
            topSpots[index].viento[h] = datos[5]; // ''
            topSpots[index].dir_viento[h] = datos[6]; // ''
            topSpots[index].dir_ola[h] = datos[7]; // ''
            topSpots[index].tipo_viento[h] = datos[8]; // ''
            topSpots[index].score[h] = datos[9]; // ''
            // aumentem en 1 el la hora per guardar les dades de la seguent hora en el seu lloc corresponent
            topSpots[index].numHoras++; 
          }
        }
        tiempoUltimoDato = millis();
      }
      // Sistema per parar el bucle del procesament de dades si el Wi-Fi es desconecta
      // o hi ha alguna falla en el sistema
      if (millis() - tiempoUltimoDato > 5000) { break; } 
    }
    // Parem la comunicació amb el servidor de python una vegada s'ha acabat de processar les dades
    // i organitzar-les en les "carpetes" internes de l'Arduino
    client.stop(); 
    // es suma 1 al índex perquè comença per 0 i el que volem es saber el nombre de platjes 
    // Variable que s'utilitza en el carrusel per saber quan s'ha de tornar a la primera platja
    totalSpotsCargados = index + 1; 
    // Verifiquem si s'ha carregat alguna platja, en cas contrari hi ha hagut un error
    if (totalSpotsCargados <= 0) { 
      mostrarError("Lectura fallida");
    }
  } else { 
    // Si l'Arduino no ha establet connexió amb el servidor de python a través de la red Wi-Fi,
    // es mostra un missatge d'error
    mostrarError("Servidor apagat");
  }
}

El programa del servidor és el següent:

import requests
from http.server import BaseHTTPRequestHandler, HTTPServer
from urllib.parse import urlparse, parse_qs
import socket
import time
from datetime import datetime
import math
# Base de dades de ubicacions
spots_db = {
    "Andalusia": [
        {"nom": "El Palmar", "lat": 36.236, "lon": -6.071, "orientacio": 240},
        {"nom": "Tarifa", "lat": 36.013, "lon": -5.606, "orientacio": 220},
        {"nom": "Yerbabuena", "lat": 36.183, "lon": -5.992, "orientacio": 250},
        {"nom": "Cabopino", "lat": 36.488, "lon": -4.744, "orientacio": 180},
        {"nom": "Punta Umbria", "lat": 37.178, "lon": -6.966, "orientacio": 230},
        {"nom": "Los Bateles", "lat": 36.273, "lon": -6.089, "orientacio": 240},
        {"nom": "Canos de Meca", "lat": 36.182, "lon": -6.011, "orientacio": 250},
        {"nom": "Cortadura", "lat": 36.494, "lon": -6.265, "orientacio": 230},
        {"nom": "Mazagon", "lat": 37.135, "lon": -6.822, "orientacio": 220},
        {"nom": "Santa Amalia", "lat": 36.536, "lon": -4.618, "orientacio": 180}
    ],
    "Asturies": [
        {"nom": "Salinas", "lat": 43.578, "lon": -5.955, "orientacio": 340},
        {"nom": "Rodiles", "lat": 43.533, "lon": -5.383, "orientacio": 10},
        {"nom": "Xago", "lat": 43.611, "lon": -5.918, "orientacio": 350},
        {"nom": "San Lorenzo", "lat": 43.543, "lon": -5.654, "orientacio": 20},
        {"nom": "Playa Espana", "lat": 43.545, "lon": -5.597, "orientacio": 0},
        {"nom": "Tapia", "lat": 43.571, "lon": -6.945, "orientacio": 330},
        {"nom": "Penarronda", "lat": 43.554, "lon": -6.992, "orientacio": 320},
        {"nom": "San Antolin", "lat": 43.438, "lon": -4.869, "orientacio": 350},
        {"nom": "Vega", "lat": 43.486, "lon": -5.143, "orientacio": 10},
        {"nom": "Frejulfe", "lat": 43.559, "lon": -6.658, "orientacio": 330}
    ],
    "Canaries": [
        {"nom": "Famara", "lat": 29.116, "lon": -13.556, "orientacio": 300},
        {"nom": "Las Americas", "lat": 28.058, "lon": -16.732, "orientacio": 210},
        {"nom": "El Quemao", "lat": 29.117, "lon": -13.633, "orientacio": 310},
        {"nom": "Los Lobos", "lat": 28.749, "lon": -13.818, "orientacio": 270},
        {"nom": "El Confital", "lat": 28.163, "lon": -15.441, "orientacio": 300},
        {"nom": "La Santa", "lat": 29.109, "lon": -13.651, "orientacio": 310},
        {"nom": "El Socorro", "lat": 28.396, "lon": -16.602, "orientacio": 350},
        {"nom": "Punta Blanca", "lat": 28.212, "lon": -16.836, "orientacio": 260},
        {"nom": "Igueste", "lat": 28.536, "lon": -16.152, "orientacio": 70},
        {"nom": "Bajamar", "lat": 28.556, "lon": -16.345, "orientacio": 30}
    ],
    "Cantabria": [
        {"nom": "Somo", "lat": 43.454, "lon": -3.765, "orientacio": 20},
        {"nom": "Los Locos", "lat": 43.435, "lon": -4.045, "orientacio": 350},
        {"nom": "Laredo", "lat": 43.415, "lon": -3.432, "orientacio": 30},
        {"nom": "Berria", "lat": 43.458, "lon": -3.468, "orientacio": 40},
        {"nom": "Liencres", "lat": 43.468, "lon": -3.938, "orientacio": 340},
        {"nom": "Meron", "lat": 43.391, "lon": -4.385, "orientacio": 350},
        {"nom": "Santa Marina", "lat": 43.453, "lon": -3.738, "orientacio": 20},
        {"nom": "Langre", "lat": 43.477, "lon": -3.702, "orientacio": 10},
        {"nom": "Galizano", "lat": 43.481, "lon": -3.674, "orientacio": 20},
        {"nom": "Suances", "lat": 43.437, "lon": -4.038, "orientacio": 350}
    ],
    "Catalunya": [
        {"nom": "Barceloneta", "lat": 41.378, "lon": 2.192, "orientacio": 120},
        {"nom": "Sitges", "lat": 41.233, "lon": 1.804, "orientacio": 180},
        {"nom": "Masnou", "lat": 41.478, "lon": 2.313, "orientacio": 110},
        {"nom": "Montgat", "lat": 41.464, "lon": 2.279, "orientacio": 120},
        {"nom": "Castelldefels", "lat": 41.264, "lon": 1.993, "orientacio": 170},
        {"nom": "Premia de Mar", "lat": 41.491, "lon": 2.359, "orientacio": 110},
        {"nom": "Bogatell", "lat": 41.393, "lon": 2.207, "orientacio": 120},
        {"nom": "Garraf", "lat": 41.253, "lon": 1.901, "orientacio": 160},
        {"nom": "Riu Besos", "lat": 41.417, "lon": 2.232, "orientacio": 110},
        {"nom": "Blanes", "lat": 41.673, "lon": 2.795, "orientacio": 130}
    ],
    "Galicia": [
        {"nom": "Razo", "lat": 43.292, "lon": -8.705, "orientacio": 300},
        {"nom": "Pantin", "lat": 43.638, "lon": -8.109, "orientacio": 310},
        {"nom": "Doninos", "lat": 43.498, "lon": -8.318, "orientacio": 300},
        {"nom": "A Lanzada", "lat": 42.433, "lon": -8.878, "orientacio": 260},
        {"nom": "Patos", "lat": 42.146, "lon": -8.824, "orientacio": 280},
        {"nom": "Nemina", "lat": 43.013, "lon": -9.231, "orientacio": 290},
        {"nom": "Sabon", "lat": 43.328, "lon": -8.504, "orientacio": 300},
        {"nom": "Bastiagueiro", "lat": 43.344, "lon": -8.349, "orientacio": 310},
        {"nom": "Soesto", "lat": 43.208, "lon": -9.022, "orientacio": 300},
        {"nom": "Rio Sieira", "lat": 42.648, "lon": -9.034, "orientacio": 280}
    ],
    "Pais_Basc": [
        {"nom": "Mundaka", "lat": 43.407, "lon": -2.697, "orientacio": 30},
        {"nom": "Zarautz", "lat": 43.285, "lon": -2.164, "orientacio": 20},
        {"nom": "Sopelana", "lat": 43.388, "lon": -2.996, "orientacio": 350},
        {"nom": "Bakio", "lat": 43.428, "lon": -2.808, "orientacio": 20},
        {"nom": "Zurriola", "lat": 43.326, "lon": -1.975, "orientacio": 40},
        {"nom": "Menakoz", "lat": 43.395, "lon": -2.984, "orientacio": 350},
        {"nom": "Laga", "lat": 43.411, "lon": -2.651, "orientacio": 30},
        {"nom": "Karraspio", "lat": 43.366, "lon": -2.497, "orientacio": 40},
        {"nom": "Orrua", "lat": 43.303, "lon": -2.224, "orientacio": 20},
        {"nom": "Playa Gris", "lat": 43.305, "lon": -2.235, "orientacio": 20}
    ]
}
def factor_swell(orientacio, dir_onada):
    diff = abs(orientacio - dir_onada)
    diff = min(diff, 360 - diff)
    return max(0, math.cos(math.radians(diff)))  # evitem negatius
def factor_vent_orientacio(orientacio, dir_vent):
    offshore = (orientacio + 180) % 360
    diff = abs(offshore - dir_vent)
    diff = min(diff, 360 - diff)
    return math.cos(math.radians(diff))
def factor_vent_velocitat(vent):
    return 1 / (1 + math.log1p(vent))
def calcular_score(onada, dir_onada, periode, vent, dir_vent, orientacio):
    fs = factor_swell(orientacio, dir_onada)
    fv_dir = factor_vent_orientacio(orientacio, dir_vent)
    fv_vel = factor_vent_velocitat(vent)
    return onada * (1 + 0.5 * fs) * (1 + 0.7 * fv_dir) *fv_vel * (periode/10)
def tipus_vent(orientacio, dir_vent):
    offshore = (orientacio + 180) % 360
    diff = abs(offshore - dir_vent)
    diff = min(diff, 360 - diff)
    if diff <= 45:
        return "OFFSHORE"
    elif diff >= 135:
        return "ONSHORE"
    else:
        return "CROSSSHORE"
def calcular_top_5_comunitat(comunitat):
    hora_actual = datetime.now().hour
    resultats = []
    llista_spots = spots_db.get(comunitat, spots_db["Cantabria"])
    print(f"\n[{comunitat}] Calculant el Top 5 per a les próximes 24h...")
    for spot in llista_spots:
        ##url = f"https://marine-api.open-meteo.com/v1/marine?latitude={spot['lat']}&longitude={spot['lon']}&hourly=wave_height,wave_period,wave_direction,wind_speed_10m,wind_direction_10m,sea_surface_temperature&forecast_days=2&timezone=Europe/Berlin"
        url_mar = f"https://marine-api.open-meteo.com/v1/marine?latitude={spot['lat']}&longitude={spot['lon']}&hourly=wave_height,wave_period,wave_direction,sea_surface_temperature&forecast_days=2&timezone=Europe/Madrid"
        url_vent = f"https://api.open-meteo.com/v1/forecast?latitude={spot['lat']}&longitude={spot['lon']}&hourly=wind_speed_10m,wind_direction_10m&forecast_days=2&timezone=Europe/Madrid"
        exito = False
        while not exito and intents < 3:
            try:
                ##resposta = requests.get(url, timeout=5).json()
                resposta_mar = requests.get(url_mar, timeout=5).json()
                resposta_vent = requests.get(url_vent, timeout=5).json()
                millor_score = 0.0
                previsio_spot = []
                for i in range(hora_actual, hora_actual + 12):
                    onada = resposta_mar["hourly"]["wave_height"][i]
                    periode = resposta_mar["hourly"]["wave_period"][i]
                    dir_onada = resposta_mar["hourly"]["wave_direction"][i]
                    vent = resposta_vent["hourly"]["wind_speed_10m"][i]
                    dir_vent = resposta_vent["hourly"]["wind_direction_10m"][i]
                    tipus = tipus_vent(spot["orientacio"], dir_vent)
                    score = calcular_score(onada,dir_onada,periode,vent,dir_vent,spot["orientacio"])
                    if score > millor_score:
                        millor_score = score
                    temps = resposta_mar["hourly"]["time"][i]
                    dia = int(temps[8:10])
                    mes = int(temps[5:7])
                    previsio_spot.append({
                        "data": f"{dia}/{mes}",
                        "hora": temps[11:16],
                        "onada": onada,
                        "periode": periode,
                        "temp": resposta_mar["hourly"]["sea_surface_temperature"][i],
                        "vent": vent,
                        "dir_vent": dir_vent,
                        "dir_onada": dir_onada,
                        "tipus_vent": tipus,
                        "score": round(score, 3)
                    })
                resultats.append({
                    "nom": spot["nom"],
                    "score": millor_score,
                    "previsio": previsio_spot
                })
                exito = True
            except Exception as e:
                intents += 1
                print(f"  -> Error llegint {spot['nom']} (Intent {intents}/3). Reintentant...")
                time.sleep(1)
        time.sleep(0.2)
    # ORDENAR PER SCORE
    resultats.sort(key=lambda x: x["score"], reverse=True)
    top_5 = resultats[:5]
    csv_final = ""
    for index, spot in enumerate(top_5):
        csv_final += f"{spot['nom']}\n"
        for p in spot['previsio']:
            csv_final += f"{p['data']},{p['hora']},{p['onada']},{p['periode']},{p['temp']},{p['vent']},{p['dir_vent']},{p['dir_onada']},{p['tipus_vent']},{p['score']}\n"
        if index < len(top_5) - 1:
            csv_final += "---\n"
    print(f"Calculant el Top 5.")
    return csv_final
class Manejador(BaseHTTPRequestHandler):
    def do_GET(self):
        parsed_path = urlparse(self.path)
        query = parse_qs(parsed_path.query)
        regio = query.get('regio', ['Cantabria'])[0] 
        print(f"\n--> Arduino ha solicitado datos de: {regio}")
        csv_data = calcular_top_5_comunitat(regio)
        self.send_response(200)
        self.send_header('Content-type', 'text/plain')
        self.end_headers()
        self.wfile.write(csv_data.encode('utf-8'))
#Unic canvi del codi tancat al codi obert. eliminem la funcio Obtenir_ip que ja no es necesari 
if __name__ == '__main__':
    # Llegim el port de la variable d'entorn que assigna el servidor (per defecte 8080 si ho corres en local)
    puerto = int(os.environ.get('PORT', 8080))
    print("="*50)
    print(f"Servidor iniciat (Port dinàmic: {puerto})")
    print("="*50)
    servidor = HTTPServer(('0.0.0.0', puerto), Manejador)
    servidor.serve_forever()

 

 

Llicència de Creative Commons
Aquesta obra d'Oriol Boix està llicenciada sota una llicència no importada Reconeixement-NoComercial-SenseObraDerivada 3.0.