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.

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()

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