diff --git a/backend/pom.xml b/backend/pom.xml index 782477a..5adfd61 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -73,6 +73,27 @@ 1.5.13 + + + org.locationtech.jts + jts-core + 1.20.0 + + + + + org.locationtech.jts.io + jts-io-common + 1.20.0 + + + + + com.fasterxml.jackson.core + jackson-core + 2.19.0 + + diff --git a/backend/src/main/java/net/miarma/contaminus/common/VoronoiZoneDetector.java b/backend/src/main/java/net/miarma/contaminus/common/VoronoiZoneDetector.java new file mode 100644 index 0000000..5ae4e51 --- /dev/null +++ b/backend/src/main/java/net/miarma/contaminus/common/VoronoiZoneDetector.java @@ -0,0 +1,100 @@ +package net.miarma.contaminus.common; + +import java.io.File; +import java.io.InputStream; +import java.net.URI; +import java.net.URL; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.List; + +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.Point; +import org.locationtech.jts.geom.Polygon; +import org.locationtech.jts.io.geojson.GeoJsonReader; + +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; + +public class VoronoiZoneDetector { + + private static class Zone { + Polygon polygon; + String actuatorId; + + public Zone(Polygon polygon, String actuatorId) { + this.polygon = polygon; + this.actuatorId = actuatorId; + } + } + + private final List zones = new ArrayList<>(); + private final GeometryFactory geometryFactory = new GeometryFactory(); + private final Gson gson = new Gson(); + + public VoronoiZoneDetector(String geojsonUrl, boolean isUrl) throws Exception { + String geojsonStr; + + if(isUrl) { + try(InputStream is = URL.of(URI.create(geojsonUrl), null).openStream()) { + geojsonStr = new String(is.readAllBytes()); + } + } else { + geojsonStr = Files.readString(new File(geojsonUrl).toPath()); + } + + JsonObject root = JsonParser.parseString(geojsonStr).getAsJsonObject(); + JsonArray features = root.getAsJsonArray("features"); + GeoJsonReader reader = new GeoJsonReader(geometryFactory); + + for (int i = 0; i < features.size(); i++) { + JsonObject feature = features.get(i).getAsJsonObject(); + + String actuatorId = feature + .getAsJsonObject("properties") + .get("actuatorId") + .getAsString(); + + JsonObject geometryJson = feature.getAsJsonObject("geometry"); + String geometryStr = gson.toJson(geometryJson); + + Geometry geometry = reader.read(geometryStr); + + if (geometry instanceof Polygon polygon) { + zones.add(new Zone(polygon, actuatorId)); + } else { + Constants.LOGGER.error("⚠️ Geometría ignorada: no es un polígono"); + } + } + } + + public String getZoneForPoint(double lon, double lat) { + Point p = geometryFactory.createPoint(new Coordinate(lon, lat)); + + for (Zone z : zones) { + if (z.polygon.covers(p)) { + return z.actuatorId; + } + } + + return null; // no está dentro de ninguna zona + } + + public static void main(String[] args) throws Exception { + VoronoiZoneDetector detector = new VoronoiZoneDetector("https://miarma.net/files/voronoi_sevilla_geovoronoi.geojson", true); + + double lon = -5.9752; + double lat = 37.3887; + + String actuatorId = detector.getZoneForPoint(lon, lat); + if (actuatorId != null) { + System.out.println("📍 El punto pertenece al actuator: " + actuatorId); + } else { + System.out.println("🚫 El punto no pertenece a ninguna zona"); + } + } +} diff --git a/backend/src/main/java/net/miarma/contaminus/verticles/LogicLayerAPIVerticle.java b/backend/src/main/java/net/miarma/contaminus/verticles/LogicLayerAPIVerticle.java index 7d0d228..c43d834 100644 --- a/backend/src/main/java/net/miarma/contaminus/verticles/LogicLayerAPIVerticle.java +++ b/backend/src/main/java/net/miarma/contaminus/verticles/LogicLayerAPIVerticle.java @@ -45,7 +45,12 @@ public class LogicLayerAPIVerticle extends AbstractVerticle { WebClientOptions options = new WebClientOptions() .setUserAgent("ContaminUS"); this.restClient = new RestClientUtil(WebClient.create(Vertx.vertx(), options)); - this.mqttClient = MqttClient.create(Vertx.vertx(), new MqttClientOptions().setAutoKeepAlive(true)); + this.mqttClient = MqttClient.create(Vertx.vertx(), + new MqttClientOptions() + .setAutoKeepAlive(true) + .setUsername("contaminus") + .setPassword("contaminus") + ); } @Override @@ -70,11 +75,25 @@ public class LogicLayerAPIVerticle extends AbstractVerticle { router.route(HttpMethod.GET, Constants.HISTORY).handler(this::getDeviceHistory); router.route(HttpMethod.GET, Constants.SENSOR_VALUES).handler(this::getSensorValues); - vertx.createHttpServer() - .requestHandler(router) - .listen(configManager.getLogicApiPort(), configManager.getHost()); - - startPromise.complete(); + mqttClient.connect(1883, "localhost", ar -> { + if (ar.succeeded()) { + Constants.LOGGER.info("🟢 MQTT client connected"); + vertx.createHttpServer() + .requestHandler(router) + .listen(configManager.getLogicApiPort(), configManager.getHost(), http -> { + if (http.succeeded()) { + Constants.LOGGER.info("🟢 HTTP server started on port " + configManager.getLogicApiPort()); + startPromise.complete(); + } else { + Constants.LOGGER.error("🔴 HTTP server failed to start: " + http.cause()); + startPromise.fail(http.cause()); + } + }); + } else { + Constants.LOGGER.error("🔴 MQTT client connection failed: " + ar.cause()); + startPromise.fail(ar.cause()); + } + }); } private void getDeviceLatestValues(RoutingContext context) { @@ -152,27 +171,18 @@ public class LogicLayerAPIVerticle extends AbstractVerticle { WeatherValue weatherValue = gson.fromJson(weather.toString(), WeatherValue.class); COValue coValue = gson.fromJson(co.toString(), COValue.class); + // MQTT publish ============================= float coAmount = coValue.getValue(); + Constants.LOGGER.info("CO amount received: " + coAmount); String topic = buildTopic(Integer.parseInt(groupId), deviceId, "matrix"); - if (coAmount >= 80.0f) { - mqttClient.connect(1883, "miarma.net", ar -> { - if(ar.succeeded()) { - Constants.LOGGER.info("Connected to MQTT broker"); - mqttClient.publish(topic, Buffer.buffer("ECO"), MqttQoS.AT_LEAST_ONCE, false, false); - } else { - Constants.LOGGER.error("Error connecting to MQTT broker: " + ar.cause().getMessage()); - } - }); - } else { - mqttClient.connect(1883, "miarma.net", ar -> { - if(ar.succeeded()) { - Constants.LOGGER.info("Connected to MQTT broker"); - mqttClient.publish(topic, Buffer.buffer("GAS"), MqttQoS.AT_LEAST_ONCE, false, false); - } else { - Constants.LOGGER.error("Error connecting to MQTT broker: " + ar.cause().getMessage()); - } - }); + Constants.LOGGER.info("Topic: " + topic); + if (mqttClient.isConnected()) { + Constants.LOGGER.info("🟢 Publishing to MQTT topic: " + topic + " with value: " + coAmount); + mqttClient.publish(topic, Buffer.buffer(coAmount >= 80.0f ? "ECO" : "GAS"), + MqttQoS.AT_LEAST_ONCE, false, false); + Constants.LOGGER.info("🟢 Message published to MQTT topic: " + topic); } + // ============================================ gpsValue.setDeviceId(deviceId); weatherValue.setDeviceId(deviceId); diff --git a/backend/src/main/java/net/miarma/contaminus/verticles/MainVerticle.java b/backend/src/main/java/net/miarma/contaminus/verticles/MainVerticle.java index 2ee8f9b..b840aad 100644 --- a/backend/src/main/java/net/miarma/contaminus/verticles/MainVerticle.java +++ b/backend/src/main/java/net/miarma/contaminus/verticles/MainVerticle.java @@ -17,7 +17,7 @@ import net.miarma.contaminus.common.Constants; public class MainVerticle extends AbstractVerticle { private ConfigManager configManager; - public static void main(String[] args) { + public static void main(String[] args) { Launcher.executeCommand("run", MainVerticle.class.getName()); } diff --git a/backend/src/main/resources/coords.txt b/backend/src/main/resources/coords.txt new file mode 100644 index 0000000..9250f9a --- /dev/null +++ b/backend/src/main/resources/coords.txt @@ -0,0 +1,18 @@ +Bellavista: 37.32533897043053, -5.968045749476732 +Bermejales: 37.3490769843888, -5.976422439823011 +Los Remedios: 37.37515235961667, -6.000317870442392 +Tiro de Línea: 37.36841756821152, -5.97598991091761 +San Bernardo: 37.3789653804891, -5.987731412877974 +Nervión: 37.38310913923199, -5.97320443871298 +Cerro del Águila: 37.3733690603403, -5.960390435428303 +Polígono Sur: 37.36134485686387, -5.965857306623599 +Amate: 37.381087013389035, -5.953545655643191 +Sevilla Este: 37.39944168632523, -5.925092000775878 +Valdezorras: 37.429014427259844, -5.927831833065477 +Pino Montano: 37.423390190328654, -5.973241522877093 +Macarena: 37.40735604254458, -5.980276144202576 +Centro: 37.39277256619283, -5.994765524658572 +Santa Justa: 37.394092596421686, -5.962662563452358 +Santa Clara: 37.40050556650672, -5.954710495719027 +Triana: 37.380789093980844, -6.008393542942082 +Cartuja: 37.403427709335816, -6.007627600183472 \ No newline at end of file diff --git a/backend/src/main/resources/voronoi.py b/backend/src/main/resources/voronoi.py new file mode 100644 index 0000000..0f55ac5 --- /dev/null +++ b/backend/src/main/resources/voronoi.py @@ -0,0 +1,70 @@ +import geopandas as gpd # esto es necesario para leer el GeoJSON +import matplotlib.pyplot as plt # esto es necesario para graficar +import numpy as np # esto es necesario para manejar los arrays +import json # esto es necesario para guardar el GeoJSON +from shapely.geometry import Polygon # esto es necesario para crear la caja de recorte +from geovoronoi import voronoi_regions_from_coords # esto es necesario para crear el Voronoi + +# se cargan los puntos (coordenadas) de los actuadores +# en formato GeoJSON +points_gdf = gpd.read_file("sevilla.geojson") +coords = np.array([[geom.x, geom.y] for geom in points_gdf.geometry]) + +# esto es una "caja" alrededor de Sevilla para recortar el Voronoi +# para que las regiones no acotadas no sean infinitas +seville_boundary = Polygon([ + (-6.10, 37.30), + (-5.85, 37.30), + (-5.85, 37.45), + (-6.10, 37.45) +]) +area_gdf = gpd.GeoDataFrame(geometry=[seville_boundary], crs="EPSG:4326") + +# se genera el Voronoi con las coordenadas de los actuadores +# y la caja de recorte (unión de todo) +region_polys, region_pts = voronoi_regions_from_coords(coords, area_gdf.union_all()) + +# dibuja con matplotlib +fig, ax = plt.subplots(figsize=(10, 10)) + +for poly in region_polys.values(): + x, y = poly.exterior.xy + ax.fill(x, y, alpha=0.3, edgecolor='black') + +ax.plot(coords[:, 0], coords[:, 1], 'ro') +for idx, coord in enumerate(coords): + ax.text(coord[0], coord[1], points_gdf.iloc[idx]["name"], fontsize=8, ha='center') + +ax.set_title("Zonas Voronoi por Actuator (GeoVoronoi)") +plt.axis("equal") +plt.grid(True) +plt.tight_layout() +plt.show() + +# Guardar GeoJSON +features = [] +for idx, region in region_polys.items(): + point_index = region_pts[idx] # índice del punto original + name = points_gdf.loc[point_index, "name"] + if isinstance(name, gpd.GeoSeries): + name = name.iloc[0] # o el que tú quieras + name = str(name) + + feature = { + "type": "Feature", + "properties": { + "actuatorId": name + }, + "geometry": json.loads(gpd.GeoSeries([region]).to_json())["features"][0]["geometry"] + } + features.append(feature) + +geojson_output = { + "type": "FeatureCollection", + "features": features +} + +with open("voronoi_sevilla_geovoronoi.geojson", "w") as f: + json.dump(geojson_output, f, indent=2) + +print("✅ GeoJSON guardado como 'voronoi_sevilla_geovoronoi.geojson'") diff --git a/frontend/public/fonts/LEDBOARD.ttf b/frontend/public/fonts/LEDBOARD.ttf new file mode 100644 index 0000000..4e8cb7c Binary files /dev/null and b/frontend/public/fonts/LEDBOARD.ttf differ diff --git a/frontend/public/fonts/OpenSans.ttf b/frontend/public/fonts/OpenSans.ttf new file mode 100644 index 0000000..ac587b4 Binary files /dev/null and b/frontend/public/fonts/OpenSans.ttf differ diff --git a/frontend/public/fonts/ProductSansBold.ttf b/frontend/public/fonts/ProductSansBold.ttf new file mode 100644 index 0000000..d847195 Binary files /dev/null and b/frontend/public/fonts/ProductSansBold.ttf differ diff --git a/frontend/public/fonts/ProductSansBoldItalic.ttf b/frontend/public/fonts/ProductSansBoldItalic.ttf new file mode 100644 index 0000000..129d12d Binary files /dev/null and b/frontend/public/fonts/ProductSansBoldItalic.ttf differ diff --git a/frontend/public/fonts/ProductSansItalic.ttf b/frontend/public/fonts/ProductSansItalic.ttf new file mode 100644 index 0000000..5fc56d4 Binary files /dev/null and b/frontend/public/fonts/ProductSansItalic.ttf differ diff --git a/frontend/public/fonts/ProductSansRegular.ttf b/frontend/public/fonts/ProductSansRegular.ttf new file mode 100644 index 0000000..c0442ee Binary files /dev/null and b/frontend/public/fonts/ProductSansRegular.ttf differ diff --git a/frontend/src/components/HistoryCharts.jsx b/frontend/src/components/HistoryCharts.jsx index 6b937c4..5a980e4 100644 --- a/frontend/src/components/HistoryCharts.jsx +++ b/frontend/src/components/HistoryCharts.jsx @@ -7,6 +7,8 @@ import { useTheme } from "@/hooks/useTheme"; import { DataProvider } from "@/context/DataContext.jsx"; import { useDataContext } from "@/hooks/useDataContext"; import { useConfig } from "@/hooks/useConfig"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faInfoCircle } from "@fortawesome/free-solid-svg-icons"; ChartJS.register(LineElement, PointElement, LinearScale, CategoryScale, Filler); @@ -54,16 +56,31 @@ const HistoryChartsContent = () => { carbonMonoxide: [] }; + const threeDaysAgo = new Date(); + threeDaysAgo.setDate(threeDaysAgo.getDate() - 3); + const isToday = (timestamp) => { + const date = new Date(timestamp * 1000); + return ( + date.getUTCFullYear() >= threeDaysAgo.getUTCFullYear() && + date.getUTCMonth() >= threeDaysAgo.getUTCMonth() && + date.getUTCDate() >= threeDaysAgo.getUTCDate() + ); + }; + data?.forEach(sensor => { - if (sensor.value != null && grouped[sensor.valueType]) { + if ( + sensor.value != null && + grouped[sensor.valueType] && + isToday(sensor.timestamp) + ) { grouped[sensor.valueType].push({ timestamp: sensor.timestamp * 1000, value: sensor.value }); - } }); + const sortAndExtract = (entries) => { const sorted = entries.sort((a, b) => a.timestamp - b.timestamp); @@ -75,45 +92,50 @@ const HistoryChartsContent = () => { }) ); -const values = sorted.map(e => e.value); -return { labels, values }; + const values = sorted.map(e => e.value); + return { labels, values }; }; -const temp = sortAndExtract(grouped.temperature); -const hum = sortAndExtract(grouped.humidity); -const press = sortAndExtract(grouped.pressure); -const co = sortAndExtract(grouped.carbonMonoxide); + const temp = sortAndExtract(grouped.temperature); + const hum = sortAndExtract(grouped.humidity); + const press = sortAndExtract(grouped.pressure); + const co = sortAndExtract(grouped.carbonMonoxide); -const timeLabels = temp.labels.length ? temp.labels : hum.labels.length ? hum.labels : co.labels.length ? co.labels : ["Sin datos"]; + const timeLabels = temp.labels.length ? temp.labels : hum.labels.length ? hum.labels : co.labels.length ? co.labels : ["Sin datos"]; -const historyData = [ - { title: "🌡️ Temperatura", data: temp.values, borderColor: "#00FF85", backgroundColor: "rgba(0, 255, 133, 0.2)" }, - { title: "💦 Humedad", data: hum.values, borderColor: "#00D4FF", backgroundColor: "rgba(0, 212, 255, 0.2)" }, - { title: "⏲ Presión", data: press.values, borderColor: "#B12424", backgroundColor: "rgba(255, 0, 0, 0.2)" }, - { title: "☁️ Contaminación", data: co.values, borderColor: "#FFA500", backgroundColor: "rgba(255, 165, 0, 0.2)" } -]; + const historyData = [ + { title: "🌡️ Temperatura", data: temp.values, borderColor: "#00FF85", backgroundColor: "rgba(0, 255, 133, 0.2)" }, + { title: "💦 Humedad", data: hum.values, borderColor: "#00D4FF", backgroundColor: "rgba(0, 212, 255, 0.2)" }, + { title: "⏲ Presión", data: press.values, borderColor: "#B12424", backgroundColor: "rgba(255, 0, 0, 0.2)" }, + { title: "☁️ Contaminación", data: co.values, borderColor: "#FFA500", backgroundColor: "rgba(255, 165, 0, 0.2)" } + ]; -return ( - ({ - title, - content: ( - - ), - styleMode: "override", - className: "col-lg-6 col-xxs-12 d-flex flex-column align-items-center p-3 card-container", - style: { minHeight: "250px" } - }))} - className="" - /> -); + return ( + <> + ({ + title, + content: ( + + ), + styleMode: "override", + className: "col-lg-6 col-xxs-12 d-flex flex-column align-items-center", + style: { minHeight: "250px" } + }))} + /> + + +

El historial muestra datos de los últimos 3 días

+
+ + ); }; HistoryCharts.propTypes = { diff --git a/frontend/src/components/PollutionMap.jsx b/frontend/src/components/PollutionMap.jsx index 8dc6c7d..ac2d762 100644 --- a/frontend/src/components/PollutionMap.jsx +++ b/frontend/src/components/PollutionMap.jsx @@ -72,9 +72,7 @@ const PollutionMapContent = () => { if (!data) return

Datos no disponibles.

; return ( -
-
-
+
); } diff --git a/frontend/src/components/SummaryCards.jsx b/frontend/src/components/SummaryCards.jsx index 551e176..2a2fb58 100644 --- a/frontend/src/components/SummaryCards.jsx +++ b/frontend/src/components/SummaryCards.jsx @@ -39,12 +39,52 @@ const SummaryCardsContent = () => { if (!data) return

Datos no disponibles.

; const CardsData = [ - { id: 1, title: "Temperatura", content: "N/A", status: "Esperando datos...", titleIcon: '🌡 ' }, - { id: 2, title: "Humedad", content: "N/A", status: "Esperando datos...", titleIcon: '💦 ' }, - { id: 3, title: "Presión", content: "N/A", status: "Esperando datos...", titleIcon: '⏲ ' }, - { id: 4, title: "Nivel de CO", content: "N/A", status: "Esperando datos...", titleIcon: '☁ ' } + { + id: 1, + title: "Temperatura", + content: "N/A", + status: "Esperando datos...", + titleIcon: '🌡 ', + className: "col-12 col-md-6 col-lg-3", + link: false, + text: true + }, + + { + id: 2, + title: "Humedad", + content: "N/A", + status: "Esperando datos...", + titleIcon: '💦 ', + className: "col-12 col-md-6 col-lg-3", + link: false, + text: true + }, + + { + id: 3, + title: "Presión", + content: "N/A", + status: "Esperando datos...", + titleIcon: '⏲ ', + className: "col-12 col-md-6 col-lg-3", + link: false, + text: true + }, + + { + id: 4, + title: "Nivel de CO", + content: "N/A", + status: "Esperando datos...", + titleIcon: '☁ ', + className: "col-12 col-md-6 col-lg-3", + link: false, + text: true + } ]; + if (data) { let coData = data[2]; let tempData = data[1]; @@ -61,7 +101,7 @@ const SummaryCardsContent = () => { } return ( - + ); } diff --git a/frontend/src/components/layout/Card.jsx b/frontend/src/components/layout/Card.jsx index f07296d..4c9c235 100644 --- a/frontend/src/components/layout/Card.jsx +++ b/frontend/src/components/layout/Card.jsx @@ -1,9 +1,22 @@ import PropTypes from "prop-types"; import { useState, useEffect, useRef } from "react"; +import { Link } from "react-router-dom"; import "@/css/Card.css"; import { useTheme } from "@/hooks/useTheme"; -const Card = ({ title, status, children, styleMode, className, titleIcon, style }) => { +const Card = ({ + title, + status, + children, + styleMode, + className, + titleIcon, + style, + link, + to, + text, + marquee +}) => { const cardRef = useRef(null); const [shortTitle, setShortTitle] = useState(title); const { theme } = useTheme(); @@ -25,37 +38,59 @@ const Card = ({ title, status, children, styleMode, className, titleIcon, style return () => window.removeEventListener("resize", checkSize); }, [title]); - return ( + const cardContent = (
-
-

- {titleIcon} - {shortTitle} -

-
{children}
- {status ? {status} : null} +

+ {titleIcon} + {shortTitle} +

+ +
+ {marquee ? ( + +

{children}

+
+ ) : text ? ( +

{children}

+ ) : ( +
{children}
+ )} +
+ + {status && {status}}
); + + return link && to + ? {cardContent} + : cardContent; }; Card.propTypes = { title: PropTypes.string.isRequired, status: PropTypes.string.isRequired, children: PropTypes.node.isRequired, - styleMode: PropTypes.oneOf(["override", ""]), - className: PropTypes.string, + styleMode: PropTypes.oneOf(["override", ""]), + className: PropTypes.string, titleIcon: PropTypes.node, style: PropTypes.object, + link: PropTypes.bool, + to: PropTypes.string, + text: PropTypes.bool, }; Card.defaultProps = { styleMode: "", + className: "", + style: {}, + link: false, + to: "", + text: false, }; export default Card; diff --git a/frontend/src/components/layout/CardContainer.jsx b/frontend/src/components/layout/CardContainer.jsx index 1e5253c..5f369ee 100644 --- a/frontend/src/components/layout/CardContainer.jsx +++ b/frontend/src/components/layout/CardContainer.jsx @@ -1,41 +1,33 @@ import Card from "./Card.jsx"; import PropTypes from "prop-types"; -import { Link } from "react-router-dom"; -const CardContainer = ({ links, cards, className, text }) => { +const CardContainer = ({ cards, className }) => { return ( -
+
{cards.map((card, index) => ( - links ? ( - - - {text - ?

{card.content}

- :
{card.content}
- } -
- - ) : ( - - {text - ?

{card.content}

- :
{card.content}
- } +
+ + {card.content} - ) +
))}
); }; CardContainer.propTypes = { - links: Boolean, - text: Boolean, cards: PropTypes.arrayOf( PropTypes.shape({ title: PropTypes.string.isRequired, content: PropTypes.string.isRequired, status: PropTypes.string.isRequired, + className: PropTypes.string, + styleMode: PropTypes.string, + style: PropTypes.object, + titleIcon: PropTypes.node, + link: PropTypes.bool, + to: PropTypes.string, + text: PropTypes.bool, }) ).isRequired, className: PropTypes.string, diff --git a/frontend/src/components/layout/Header.jsx b/frontend/src/components/layout/Header.jsx index 51ea32d..d0dfab0 100644 --- a/frontend/src/components/layout/Header.jsx +++ b/frontend/src/components/layout/Header.jsx @@ -7,17 +7,13 @@ const Header = ({ subtitle }) => { const { theme } = useTheme(); return ( -
+

{subtitle}

- {/* */}
); } diff --git a/frontend/src/css/Card.css b/frontend/src/css/Card.css index 90c96cc..eae7d0e 100644 --- a/frontend/src/css/Card.css +++ b/frontend/src/css/Card.css @@ -46,6 +46,14 @@ margin-right: 10px; } +.card.led marquee > p.card-text { + font-family: "LEDBOARD" !important; + color: rgb(38, 60, 229) !important; + font-size: 2.5em !important; + text-transform: uppercase !important; + letter-spacing: 1px !important; +} + p.card-text { font-size: 2.2em; font-weight: 600; diff --git a/frontend/src/css/Header.css b/frontend/src/css/Header.css index f30a968..f35991c 100644 --- a/frontend/src/css/Header.css +++ b/frontend/src/css/Header.css @@ -34,6 +34,21 @@ header > .subtitle { animation: fadeIn 2s ease-in-out; } +.animated-header { + animation: pulseHeader 6s ease-in-out infinite; +} + +@keyframes pulseHeader { + 0%, 100% { + opacity: 0.96; + transform: translateY(3px); + } + 50% { + opacity: 1; + transform: translateY(-3px); + } +} + @keyframes fadeIn { 0% { opacity: 0; } 100% { opacity: 1; } diff --git a/frontend/src/css/index.css b/frontend/src/css/index.css index 2cd4958..e1f0169 100644 --- a/frontend/src/css/index.css +++ b/frontend/src/css/index.css @@ -12,42 +12,52 @@ --card-gradient-secondary: #353535; } -/* latin-ext */ @font-face { - font-family: 'Poppins'; - font-style: normal; - font-weight: 400; - font-display: swap; - src: url(https://fonts.gstatic.com/s/poppins/v22/pxiEyp8kv8JHgFVrJJnecmNE.woff2) format('woff2'); - unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; + font-family: "Open Sans"; + src: url('/fonts/OpenSans.ttf'); } -/* latin */ @font-face { - font-family: 'Poppins'; - font-style: normal; - font-weight: 400; - font-display: swap; - src: url(https://fonts.gstatic.com/s/poppins/v22/pxiEyp8kv8JHgFVrJJfecg.woff2) format('woff2'); - unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; + font-family: "Product Sans"; + src: url('/fonts/ProductSansRegular.ttf'); } -/* latin-ext */ @font-face { - font-family: 'Poppins'; - font-style: normal; - font-weight: 600; - font-display: swap; - src: url(https://fonts.gstatic.com/s/poppins/v22/pxiByp8kv8JHgFVrLEj6Z1JlFc-K.woff2) format('woff2'); - unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; + font-family: "Product Sans Italic"; + src: url('/fonts/ProductSansItalic.ttf'); } -/* latin */ @font-face { - font-family: 'Poppins'; - font-style: normal; - font-weight: 600; - font-display: swap; - src: url(https://fonts.gstatic.com/s/poppins/v22/pxiByp8kv8JHgFVrLEj6Z1xlFQ.woff2) format('woff2'); - unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; + font-family: "Product Sans Italic Bold"; + src: url('/fonts/ProductSansBoldItalic.ttf'); } + +@font-face { + font-family: "Product Sans Bold"; + src: url('/fonts/ProductSansBold.ttf'); +} + +@font-face { + font-family: "LEDBOARD"; + src: url('/fonts/LEDBOARD.ttf'); +} + +/* Tipografía global */ +div, +label, +input, +p, +span, +a, +button { + font-family: "Open Sans", sans-serif; +} + +h1, +h2, +h3, +h4, +h5, +h6 { + font-family: "Product Sans", sans-serif; +} \ No newline at end of file diff --git a/frontend/src/pages/Dashboard.jsx b/frontend/src/pages/Dashboard.jsx index f5ca237..6c50e2e 100644 --- a/frontend/src/pages/Dashboard.jsx +++ b/frontend/src/pages/Dashboard.jsx @@ -8,7 +8,7 @@ const Dashboard = () => { const { groupId, deviceId } = useParams(); return ( -
+
diff --git a/frontend/src/pages/GroupView.jsx b/frontend/src/pages/GroupView.jsx index ee7b6c4..1f85910 100644 --- a/frontend/src/pages/GroupView.jsx +++ b/frontend/src/pages/GroupView.jsx @@ -9,8 +9,10 @@ import { useEffect, useState } from "react"; import { DataProvider } from "@/context/DataContext"; import { MapContainer, TileLayer, Marker } from 'react-leaflet'; -import L from 'leaflet'; +import L, { map } from 'leaflet'; import 'leaflet/dist/leaflet.css'; +import PropTypes from 'prop-types'; +import { text } from "@fortawesome/fontawesome-svg-core"; // Icono de marcador por defecto (porque Leaflet no lo carga bien en algunos setups) const markerIcon = new L.Icon({ @@ -19,6 +21,7 @@ const markerIcon = new L.Icon({ iconAnchor: [12, 41], }); + const MiniMap = ({ lat, lon }) => ( ( ); +MiniMap.propTypes = { + lat: PropTypes.number.isRequired, + lon: PropTypes.number.isRequired, +}; + const GroupView = () => { const { groupId } = useParams(); const { config, configLoading } = useConfig(); @@ -91,21 +99,20 @@ const GroupViewContent = () => { return ( { const latest = latestData[device.deviceId]; const gpsSensor = latest?.data[0]; - const mapPreview = gpsSensor?.lat && gpsSensor?.lon - ? - : "Sin posición"; + const mapPreview = ; return { title: device.deviceName, status: `ID: ${device.deviceId}`, - content: mapPreview, + link: gpsSensor != undefined, + text: gpsSensor == undefined, + marquee: gpsSensor == undefined, + content: gpsSensor == undefined ? "SOLO VEHICULOS ELECTRICOS" : mapPreview, to: `/groups/${groupId}/devices/${device.deviceId}`, - styleMode: "override", - className: "col-12 col-md-6 col-lg-4" + className: `col-12 col-md-6 col-lg-4 ${gpsSensor == undefined ? "led" : ""}`, }; })} diff --git a/frontend/src/pages/Groups.jsx b/frontend/src/pages/Groups.jsx index 6454032..591d46a 100644 --- a/frontend/src/pages/Groups.jsx +++ b/frontend/src/pages/Groups.jsx @@ -60,17 +60,16 @@ const GroupsContent = ({ config }) => { return ( { const groupDevices = devices[group.groupId]?.data; const deviceCount = groupDevices?.length; return { title: group.groupName, + link: true, + text: true, status: `ID: ${group.groupId}`, to: `/groups/${group.groupId}`, - styleMode: "override", content: deviceCount != null ? (deviceCount === 1 ? "1 dispositivo" : `${deviceCount} dispositivos`) : "Cargando dispositivos...", diff --git a/hardware/include/WifiConnection.hpp b/hardware/include/WifiConnection.hpp index ace4661..35b1f7e 100644 --- a/hardware/include/WifiConnection.hpp +++ b/hardware/include/WifiConnection.hpp @@ -3,7 +3,7 @@ #include #include -#define SSID "iPhone de Álvaro" -#define WIFI_PASSWORD "alvarito123" +#define SSID "DIGIFIBRA-D2ys" +#define WIFI_PASSWORD "4EEATsyTcZ" -int setupWifi(); \ No newline at end of file +int WiFi_Init(); \ No newline at end of file diff --git a/hardware/include/globals.hpp b/hardware/include/globals.hpp index bfe14bf..15968d5 100644 --- a/hardware/include/globals.hpp +++ b/hardware/include/globals.hpp @@ -10,7 +10,13 @@ #define GPS_ID 1 #define MAX7219_ID 1 +#define ECO "Solo vehiculos electricos/hibridos" +#define ALL "Todo tipo de vehiculos" + #define DEBUG +#define SENSOR 0 +#define ACTUATOR 1 + extern const uint32_t DEVICE_ID; extern const int GROUP_ID; \ No newline at end of file diff --git a/hardware/include/main.hpp b/hardware/include/main.hpp index 0fe0e85..7fa4211 100644 --- a/hardware/include/main.hpp +++ b/hardware/include/main.hpp @@ -1,14 +1,29 @@ #pragma once #include "globals.hpp" + +#define DEVICE_ROLE SENSOR // se cambia entre SENSOR y ACTUATOR + +#if DEVICE_ROLE == SENSOR + #warning "Compilando firmware para SENSOR" +#elif DEVICE_ROLE == ACTUATOR + #warning "Compilando firmware para ACTUATOR" +#else + #warning "DEVICE_ROLE no definido correctamente" +#endif + #include "JsonTools.hpp" #include "RestClient.hpp" #include "WifiConnection.hpp" #include "MqttClient.hpp" +#if DEVICE_ROLE == SENSOR #include "BME280.hpp" #include "GPS.hpp" -#include "MAX7219.hpp" #include "MQ7v2.hpp" +#endif +#if DEVICE_ROLE == ACTUATOR +#include "MAX7219.hpp" +#endif struct TaskTimer { diff --git a/hardware/src/lib/inet/MqttClient.cpp b/hardware/src/lib/inet/MqttClient.cpp index f7c9af3..a1982ce 100644 --- a/hardware/src/lib/inet/MqttClient.cpp +++ b/hardware/src/lib/inet/MqttClient.cpp @@ -20,14 +20,16 @@ void MQTT_OnReceived(char *topic, byte *payload, unsigned int length) content.concat((char)payload[i]); } +#if DEVICE_ROLE == ACTUATOR if(content == "ECO") { - currentMessage = "Solo vehiculos electricos/hibridos"; + currentMessage = ECO; } else { - currentMessage = "Todo tipo de vehiculos"; + currentMessage = ALL; } +#endif } void MQTT_Init(const char *MQTTServerAddress, uint16_t MQTTServerPort) diff --git a/hardware/src/lib/inet/WifiConnection.cpp b/hardware/src/lib/inet/WifiConnection.cpp index 627272b..010f9ed 100644 --- a/hardware/src/lib/inet/WifiConnection.cpp +++ b/hardware/src/lib/inet/WifiConnection.cpp @@ -34,7 +34,7 @@ void hueCycle(uint8_t pos) setColor(r, g, b); } -int setupWifi() +int WiFi_Init() { setupLED(); diff --git a/hardware/src/main.cpp b/hardware/src/main.cpp index 2f80d0f..4dbce40 100644 --- a/hardware/src/main.cpp +++ b/hardware/src/main.cpp @@ -1,47 +1,67 @@ #include "main.hpp" const uint32_t DEVICE_ID = getChipID(); +const String mqttId = "CUS-" + String(DEVICE_ID, HEX); const int GROUP_ID = 1; -const char *currentMessage = nullptr; -const String id = "CUS-" + String(DEVICE_ID, HEX); -TaskTimer matrixTimer{0, 25}; TaskTimer globalTimer{0, 60000}; TaskTimer mqttTimer{0, 5000}; +#if DEVICE_ROLE == ACTUATOR +TaskTimer matrixTimer{0, 25}; +const char *currentMessage = ALL; +extern MD_Parola display; +#endif + extern HTTPClient httpClient; String response; -extern MD_Parola display; +#if DEVICE_ROLE == SENSOR MQ7Data_t mq7Data; BME280Data_t bme280Data; GPSData_t gpsData; +#endif void setup() { Serial.begin(115200); +#ifdef DEBUG Serial.println("Iniciando..."); +#endif - setupWifi(); + WiFi_Init(); MQTT_Init(MQTT_URI, MQTT_PORT); - BME280_Init(); - Serial.println("Sensor BME280 inicializado"); - GPS_Init(); - Serial.println("Sensor GPS inicializado"); - MQ7_Init(); - Serial.println("Sensor MQ7 inicializado"); - MAX7219_Init(); - Serial.println("Display inicializado"); + try + { + +#if DEVICE_ROLE == SENSOR + BME280_Init(); + Serial.println("Sensor BME280 inicializado"); + GPS_Init(); + Serial.println("Sensor GPS inicializado"); + MQ7_Init(); + Serial.println("Sensor MQ7 inicializado"); +#endif - writeMatrix(currentMessage); +#if DEVICE_ROLE == ACTUATOR + MAX7219_Init(); + Serial.println("Display inicializado"); + writeMatrix(currentMessage); +#endif + } + catch (const char *e) + { + Serial.println(e); + } } void loop() { uint32_t now = millis(); +#if DEVICE_ROLE == ACTUATOR if (now - matrixTimer.lastRun >= matrixTimer.interval) { if (MAX7219_Animate()) @@ -50,9 +70,11 @@ void loop() } matrixTimer.lastRun = now; } +#endif if (now - globalTimer.lastRun >= globalTimer.interval) { +#if DEVICE_ROLE == SENSOR readBME280(); readGPS(); readMQ7(); @@ -62,17 +84,29 @@ void loop() #endif sendSensorData(); - +#endif globalTimer.lastRun = now; } if (now - mqttTimer.lastRun >= mqttTimer.interval) { - MQTT_Handle(id.c_str()); + MQTT_Handle(mqttId.c_str()); mqttTimer.lastRun = now; } } +#if DEVICE_ROLE == ACTUATOR +void writeMatrix(const char *message) +{ +#ifdef DEBUG + Serial.println("Escribiendo en el display..."); +#endif + + MAX7219_DisplayText(message, PA_LEFT, 50, 0); +} +#endif + +#if DEVICE_ROLE == SENSOR void readMQ7() { const float CO_THRESHOLD = 100.0f; @@ -89,15 +123,6 @@ void readGPS() gpsData = GPS_Read_Fake(); } -void writeMatrix(const char *message) -{ -#ifdef DEBUG - Serial.println("Escribiendo en el display..."); -#endif - - MAX7219_DisplayText(message, PA_LEFT, 50, 0); -} - void printAllData() { Serial.println("---------------------"); @@ -130,7 +155,6 @@ void sendSensorData() { const String deviceId = String(DEVICE_ID, HEX); - // Validaciones básicas (puedes añadir más si quieres) bool gpsValid = gpsData.lat != 0.0f && gpsData.lon != 0.0f; bool weatherValid = bme280Data.temperature != 0.0f && bme280Data.humidity != 0.0f && @@ -156,10 +180,11 @@ void sendSensorData() postRequest(String(API_URI) + "/batch", json, response); #ifdef DEBUG - Serial.println("📬 Respuesta del servidor:"); + Serial.println("📥 Respuesta del servidor:"); Serial.println(response); #endif } +#endif uint32_t getChipID() {