Implemented (partially) Voronoi algorithm for zone-dividing in Seville map. Also refactored some things in frontend. Modified hardware firmware for conditional compilation for both SENSOR and ACTUATOR type boards.
This commit is contained in:
@@ -73,6 +73,27 @@
|
|||||||
<version>1.5.13</version>
|
<version>1.5.13</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- https://mvnrepository.com/artifact/org.locationtech.jts/jts-core -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.locationtech.jts</groupId>
|
||||||
|
<artifactId>jts-core</artifactId>
|
||||||
|
<version>1.20.0</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- https://mvnrepository.com/artifact/org.locationtech.jts.io/jts-io-common -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.locationtech.jts.io</groupId>
|
||||||
|
<artifactId>jts-io-common</artifactId>
|
||||||
|
<version>1.20.0</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-core -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.fasterxml.jackson.core</groupId>
|
||||||
|
<artifactId>jackson-core</artifactId>
|
||||||
|
<version>2.19.0</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
|
|||||||
@@ -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<Zone> 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -45,7 +45,12 @@ public class LogicLayerAPIVerticle extends AbstractVerticle {
|
|||||||
WebClientOptions options = new WebClientOptions()
|
WebClientOptions options = new WebClientOptions()
|
||||||
.setUserAgent("ContaminUS");
|
.setUserAgent("ContaminUS");
|
||||||
this.restClient = new RestClientUtil(WebClient.create(Vertx.vertx(), options));
|
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
|
@Override
|
||||||
@@ -70,11 +75,25 @@ public class LogicLayerAPIVerticle extends AbstractVerticle {
|
|||||||
router.route(HttpMethod.GET, Constants.HISTORY).handler(this::getDeviceHistory);
|
router.route(HttpMethod.GET, Constants.HISTORY).handler(this::getDeviceHistory);
|
||||||
router.route(HttpMethod.GET, Constants.SENSOR_VALUES).handler(this::getSensorValues);
|
router.route(HttpMethod.GET, Constants.SENSOR_VALUES).handler(this::getSensorValues);
|
||||||
|
|
||||||
vertx.createHttpServer()
|
mqttClient.connect(1883, "localhost", ar -> {
|
||||||
.requestHandler(router)
|
if (ar.succeeded()) {
|
||||||
.listen(configManager.getLogicApiPort(), configManager.getHost());
|
Constants.LOGGER.info("🟢 MQTT client connected");
|
||||||
|
vertx.createHttpServer()
|
||||||
startPromise.complete();
|
.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) {
|
private void getDeviceLatestValues(RoutingContext context) {
|
||||||
@@ -152,27 +171,18 @@ public class LogicLayerAPIVerticle extends AbstractVerticle {
|
|||||||
WeatherValue weatherValue = gson.fromJson(weather.toString(), WeatherValue.class);
|
WeatherValue weatherValue = gson.fromJson(weather.toString(), WeatherValue.class);
|
||||||
COValue coValue = gson.fromJson(co.toString(), COValue.class);
|
COValue coValue = gson.fromJson(co.toString(), COValue.class);
|
||||||
|
|
||||||
|
// MQTT publish =============================
|
||||||
float coAmount = coValue.getValue();
|
float coAmount = coValue.getValue();
|
||||||
|
Constants.LOGGER.info("CO amount received: " + coAmount);
|
||||||
String topic = buildTopic(Integer.parseInt(groupId), deviceId, "matrix");
|
String topic = buildTopic(Integer.parseInt(groupId), deviceId, "matrix");
|
||||||
if (coAmount >= 80.0f) {
|
Constants.LOGGER.info("Topic: " + topic);
|
||||||
mqttClient.connect(1883, "miarma.net", ar -> {
|
if (mqttClient.isConnected()) {
|
||||||
if(ar.succeeded()) {
|
Constants.LOGGER.info("🟢 Publishing to MQTT topic: " + topic + " with value: " + coAmount);
|
||||||
Constants.LOGGER.info("Connected to MQTT broker");
|
mqttClient.publish(topic, Buffer.buffer(coAmount >= 80.0f ? "ECO" : "GAS"),
|
||||||
mqttClient.publish(topic, Buffer.buffer("ECO"), MqttQoS.AT_LEAST_ONCE, false, false);
|
MqttQoS.AT_LEAST_ONCE, false, false);
|
||||||
} else {
|
Constants.LOGGER.info("🟢 Message published to MQTT topic: " + topic);
|
||||||
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());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
// ============================================
|
||||||
|
|
||||||
gpsValue.setDeviceId(deviceId);
|
gpsValue.setDeviceId(deviceId);
|
||||||
weatherValue.setDeviceId(deviceId);
|
weatherValue.setDeviceId(deviceId);
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import net.miarma.contaminus.common.Constants;
|
|||||||
public class MainVerticle extends AbstractVerticle {
|
public class MainVerticle extends AbstractVerticle {
|
||||||
private ConfigManager configManager;
|
private ConfigManager configManager;
|
||||||
|
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
Launcher.executeCommand("run", MainVerticle.class.getName());
|
Launcher.executeCommand("run", MainVerticle.class.getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
18
backend/src/main/resources/coords.txt
Normal file
18
backend/src/main/resources/coords.txt
Normal file
@@ -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
|
||||||
70
backend/src/main/resources/voronoi.py
Normal file
70
backend/src/main/resources/voronoi.py
Normal file
@@ -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'")
|
||||||
BIN
frontend/public/fonts/LEDBOARD.ttf
Normal file
BIN
frontend/public/fonts/LEDBOARD.ttf
Normal file
Binary file not shown.
BIN
frontend/public/fonts/OpenSans.ttf
Normal file
BIN
frontend/public/fonts/OpenSans.ttf
Normal file
Binary file not shown.
BIN
frontend/public/fonts/ProductSansBold.ttf
Normal file
BIN
frontend/public/fonts/ProductSansBold.ttf
Normal file
Binary file not shown.
BIN
frontend/public/fonts/ProductSansBoldItalic.ttf
Normal file
BIN
frontend/public/fonts/ProductSansBoldItalic.ttf
Normal file
Binary file not shown.
BIN
frontend/public/fonts/ProductSansItalic.ttf
Normal file
BIN
frontend/public/fonts/ProductSansItalic.ttf
Normal file
Binary file not shown.
BIN
frontend/public/fonts/ProductSansRegular.ttf
Normal file
BIN
frontend/public/fonts/ProductSansRegular.ttf
Normal file
Binary file not shown.
@@ -7,6 +7,8 @@ import { useTheme } from "@/hooks/useTheme";
|
|||||||
import { DataProvider } from "@/context/DataContext.jsx";
|
import { DataProvider } from "@/context/DataContext.jsx";
|
||||||
import { useDataContext } from "@/hooks/useDataContext";
|
import { useDataContext } from "@/hooks/useDataContext";
|
||||||
import { useConfig } from "@/hooks/useConfig";
|
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);
|
ChartJS.register(LineElement, PointElement, LinearScale, CategoryScale, Filler);
|
||||||
|
|
||||||
@@ -54,16 +56,31 @@ const HistoryChartsContent = () => {
|
|||||||
carbonMonoxide: []
|
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 => {
|
data?.forEach(sensor => {
|
||||||
if (sensor.value != null && grouped[sensor.valueType]) {
|
if (
|
||||||
|
sensor.value != null &&
|
||||||
|
grouped[sensor.valueType] &&
|
||||||
|
isToday(sensor.timestamp)
|
||||||
|
) {
|
||||||
grouped[sensor.valueType].push({
|
grouped[sensor.valueType].push({
|
||||||
timestamp: sensor.timestamp * 1000,
|
timestamp: sensor.timestamp * 1000,
|
||||||
value: sensor.value
|
value: sensor.value
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
const sortAndExtract = (entries) => {
|
const sortAndExtract = (entries) => {
|
||||||
const sorted = entries.sort((a, b) => a.timestamp - b.timestamp);
|
const sorted = entries.sort((a, b) => a.timestamp - b.timestamp);
|
||||||
|
|
||||||
@@ -75,45 +92,50 @@ const HistoryChartsContent = () => {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const values = sorted.map(e => e.value);
|
const values = sorted.map(e => e.value);
|
||||||
return { labels, values };
|
return { labels, values };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const temp = sortAndExtract(grouped.temperature);
|
const temp = sortAndExtract(grouped.temperature);
|
||||||
const hum = sortAndExtract(grouped.humidity);
|
const hum = sortAndExtract(grouped.humidity);
|
||||||
const press = sortAndExtract(grouped.pressure);
|
const press = sortAndExtract(grouped.pressure);
|
||||||
const co = sortAndExtract(grouped.carbonMonoxide);
|
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 = [
|
const historyData = [
|
||||||
{ title: "🌡️ Temperatura", data: temp.values, borderColor: "#00FF85", backgroundColor: "rgba(0, 255, 133, 0.2)" },
|
{ 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: "💦 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: "⏲ 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)" }
|
{ title: "☁️ Contaminación", data: co.values, borderColor: "#FFA500", backgroundColor: "rgba(255, 165, 0, 0.2)" }
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CardContainer
|
<>
|
||||||
cards={historyData.map(({ title, data, borderColor, backgroundColor }) => ({
|
<CardContainer
|
||||||
title,
|
cards={historyData.map(({ title, data, borderColor, backgroundColor }) => ({
|
||||||
content: (
|
title,
|
||||||
<Line style={{ minHeight: "250px" }}
|
content: (
|
||||||
data={{
|
<Line style={{ minHeight: "250px" }}
|
||||||
labels: timeLabels,
|
data={{
|
||||||
datasets: [{ data, borderColor, backgroundColor, fill: true, tension: 0.4 }]
|
labels: timeLabels,
|
||||||
}}
|
datasets: [{ data, borderColor, backgroundColor, fill: true, tension: 0.4 }]
|
||||||
options={options}
|
}}
|
||||||
/>
|
options={options}
|
||||||
),
|
/>
|
||||||
styleMode: "override",
|
),
|
||||||
className: "col-lg-6 col-xxs-12 d-flex flex-column align-items-center p-3 card-container",
|
styleMode: "override",
|
||||||
style: { minHeight: "250px" }
|
className: "col-lg-6 col-xxs-12 d-flex flex-column align-items-center",
|
||||||
}))}
|
style: { minHeight: "250px" }
|
||||||
className=""
|
}))}
|
||||||
/>
|
/>
|
||||||
);
|
<span className="m-0 p-0 d-flex align-items-center justify-content-center">
|
||||||
|
<FontAwesomeIcon icon={faInfoCircle} className="me-2" />
|
||||||
|
<p className="m-0 p-0">El historial muestra datos de los últimos 3 días</p>
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
HistoryCharts.propTypes = {
|
HistoryCharts.propTypes = {
|
||||||
|
|||||||
@@ -72,9 +72,7 @@ const PollutionMapContent = () => {
|
|||||||
if (!data) return <p>Datos no disponibles.</p>;
|
if (!data) return <p>Datos no disponibles.</p>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-3">
|
<div id="map" className='rounded-4' style={{ height: "60vh" }}></div>
|
||||||
<div id="map" className='rounded-4' style={{ height: "60vh" }}></div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -39,12 +39,52 @@ const SummaryCardsContent = () => {
|
|||||||
if (!data) return <p>Datos no disponibles.</p>;
|
if (!data) return <p>Datos no disponibles.</p>;
|
||||||
|
|
||||||
const CardsData = [
|
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: 1,
|
||||||
{ id: 3, title: "Presión", content: "N/A", status: "Esperando datos...", titleIcon: '⏲ ' },
|
title: "Temperatura",
|
||||||
{ id: 4, title: "Nivel de CO", content: "N/A", status: "Esperando datos...", titleIcon: '☁ ' }
|
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) {
|
if (data) {
|
||||||
let coData = data[2];
|
let coData = data[2];
|
||||||
let tempData = data[1];
|
let tempData = data[1];
|
||||||
@@ -61,7 +101,7 @@ const SummaryCardsContent = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CardContainer text cards={CardsData} />
|
<CardContainer cards={CardsData} />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,22 @@
|
|||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
import { useState, useEffect, useRef } from "react";
|
import { useState, useEffect, useRef } from "react";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
import "@/css/Card.css";
|
import "@/css/Card.css";
|
||||||
import { useTheme } from "@/hooks/useTheme";
|
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 cardRef = useRef(null);
|
||||||
const [shortTitle, setShortTitle] = useState(title);
|
const [shortTitle, setShortTitle] = useState(title);
|
||||||
const { theme } = useTheme();
|
const { theme } = useTheme();
|
||||||
@@ -25,37 +38,59 @@ const Card = ({ title, status, children, styleMode, className, titleIcon, style
|
|||||||
return () => window.removeEventListener("resize", checkSize);
|
return () => window.removeEventListener("resize", checkSize);
|
||||||
}, [title]);
|
}, [title]);
|
||||||
|
|
||||||
return (
|
const cardContent = (
|
||||||
<div
|
<div
|
||||||
ref={cardRef}
|
ref={cardRef}
|
||||||
className={styleMode === "override" ? `${className}` :
|
className={`card p-3 w-100 ${theme} ${className ?? ""}`}
|
||||||
`col-xl-3 col-sm-6 d-flex flex-column align-items-center p-3 card-container ${className}`}
|
style={styleMode === "override" ? style : {}}
|
||||||
|
|
||||||
>
|
>
|
||||||
<div className={`card p-3 w-100 ${theme}`} style={styleMode === "override" ? style : {}}>
|
<h3 className="text-center">
|
||||||
<h3 className="text-center">
|
{titleIcon}
|
||||||
{titleIcon}
|
{shortTitle}
|
||||||
{shortTitle}
|
</h3>
|
||||||
</h3>
|
|
||||||
<div className="card-content">{children}</div>
|
<div className="card-content">
|
||||||
{status ? <span className="status text-center mt-2">{status}</span> : null}
|
{marquee ? (
|
||||||
|
<marquee>
|
||||||
|
<p className="card-text text-center">{children}</p>
|
||||||
|
</marquee>
|
||||||
|
) : text ? (
|
||||||
|
<p className="card-text text-center">{children}</p>
|
||||||
|
) : (
|
||||||
|
<div className="my-2">{children}</div>
|
||||||
|
)}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{status && <span className="status text-center mt-2">{status}</span>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return link && to
|
||||||
|
? <Link to={to} style={{ textDecoration: "none" }}>{cardContent}</Link>
|
||||||
|
: cardContent;
|
||||||
};
|
};
|
||||||
|
|
||||||
Card.propTypes = {
|
Card.propTypes = {
|
||||||
title: PropTypes.string.isRequired,
|
title: PropTypes.string.isRequired,
|
||||||
status: PropTypes.string.isRequired,
|
status: PropTypes.string.isRequired,
|
||||||
children: PropTypes.node.isRequired,
|
children: PropTypes.node.isRequired,
|
||||||
styleMode: PropTypes.oneOf(["override", ""]),
|
styleMode: PropTypes.oneOf(["override", ""]),
|
||||||
className: PropTypes.string,
|
className: PropTypes.string,
|
||||||
titleIcon: PropTypes.node,
|
titleIcon: PropTypes.node,
|
||||||
style: PropTypes.object,
|
style: PropTypes.object,
|
||||||
|
link: PropTypes.bool,
|
||||||
|
to: PropTypes.string,
|
||||||
|
text: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
Card.defaultProps = {
|
Card.defaultProps = {
|
||||||
styleMode: "",
|
styleMode: "",
|
||||||
|
className: "",
|
||||||
|
style: {},
|
||||||
|
link: false,
|
||||||
|
to: "",
|
||||||
|
text: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Card;
|
export default Card;
|
||||||
|
|||||||
@@ -1,41 +1,33 @@
|
|||||||
import Card from "./Card.jsx";
|
import Card from "./Card.jsx";
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
import { Link } from "react-router-dom";
|
|
||||||
|
|
||||||
const CardContainer = ({ links, cards, className, text }) => {
|
const CardContainer = ({ cards, className }) => {
|
||||||
return (
|
return (
|
||||||
<div className={`row justify-content-center g-0 ${className}`}>
|
<div className={`row justify-content-center g-3 ${className}`}>
|
||||||
{cards.map((card, index) => (
|
{cards.map((card, index) => (
|
||||||
links ? (
|
<div key={index} className={card.className ?? "col-12 col-md-6 col-lg-3"}>
|
||||||
<Link to={card.to} key={index} style={{ textDecoration: 'none' }}>
|
<Card {...card}>
|
||||||
<Card title={card.title} status={card.status} styleMode={card.styleMode} className={card.className} titleIcon={card.titleIcon} style={card.style}>
|
{card.content}
|
||||||
{text
|
|
||||||
? <p className="card-text text-center">{card.content}</p>
|
|
||||||
: <div className="my-2">{card.content}</div>
|
|
||||||
}
|
|
||||||
</Card>
|
|
||||||
</Link>
|
|
||||||
) : (
|
|
||||||
<Card key={index} title={card.title} status={card.status} styleMode={card.styleMode} className={card.className} titleIcon={card.titleIcon} style={card.style}>
|
|
||||||
{text
|
|
||||||
? <p className="card-text text-center">{card.content}</p>
|
|
||||||
: <div className="my-2">{card.content}</div>
|
|
||||||
}
|
|
||||||
</Card>
|
</Card>
|
||||||
)
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
CardContainer.propTypes = {
|
CardContainer.propTypes = {
|
||||||
links: Boolean,
|
|
||||||
text: Boolean,
|
|
||||||
cards: PropTypes.arrayOf(
|
cards: PropTypes.arrayOf(
|
||||||
PropTypes.shape({
|
PropTypes.shape({
|
||||||
title: PropTypes.string.isRequired,
|
title: PropTypes.string.isRequired,
|
||||||
content: PropTypes.string.isRequired,
|
content: PropTypes.string.isRequired,
|
||||||
status: 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,
|
).isRequired,
|
||||||
className: PropTypes.string,
|
className: PropTypes.string,
|
||||||
|
|||||||
@@ -7,17 +7,13 @@ const Header = ({ subtitle }) => {
|
|||||||
const { theme } = useTheme();
|
const { theme } = useTheme();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className={`row justify-content-center text-center mb-4 ${theme}`}>
|
<header className={`animated-header row justify-content-center text-center mb-4 ${theme}`}>
|
||||||
<div className='col-xl-4 col-lg-6 col-8'>
|
<div className='col-xl-4 col-lg-6 col-8'>
|
||||||
<Link to="/" className="text-decoration-none">
|
<Link to="/" className="text-decoration-none">
|
||||||
<img src={`/images/logo-${theme}.png`} className='img-fluid' />
|
<img src={`/images/logo-${theme}.png`} className='img-fluid' />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<p className='col-12 text-center my-3'>{subtitle}</p>
|
<p className='col-12 text-center my-3'>{subtitle}</p>
|
||||||
{/*<nav className='d-flex justify-content-center gap-4 my-3'>
|
|
||||||
<Link to="/" className="nav-link">Inicio</Link>
|
|
||||||
<Link to="/groups" className="nav-link">Grupos</Link>
|
|
||||||
</nav> */}
|
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,6 +46,14 @@
|
|||||||
margin-right: 10px;
|
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 {
|
p.card-text {
|
||||||
font-size: 2.2em;
|
font-size: 2.2em;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
|||||||
@@ -34,6 +34,21 @@ header > .subtitle {
|
|||||||
animation: fadeIn 2s ease-in-out;
|
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 {
|
@keyframes fadeIn {
|
||||||
0% { opacity: 0; }
|
0% { opacity: 0; }
|
||||||
100% { opacity: 1; }
|
100% { opacity: 1; }
|
||||||
|
|||||||
@@ -12,42 +12,52 @@
|
|||||||
--card-gradient-secondary: #353535;
|
--card-gradient-secondary: #353535;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* latin-ext */
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Poppins';
|
font-family: "Open Sans";
|
||||||
font-style: normal;
|
src: url('/fonts/OpenSans.ttf');
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* latin */
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Poppins';
|
font-family: "Product Sans";
|
||||||
font-style: normal;
|
src: url('/fonts/ProductSansRegular.ttf');
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* latin-ext */
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Poppins';
|
font-family: "Product Sans Italic";
|
||||||
font-style: normal;
|
src: url('/fonts/ProductSansItalic.ttf');
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* latin */
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Poppins';
|
font-family: "Product Sans Italic Bold";
|
||||||
font-style: normal;
|
src: url('/fonts/ProductSansBoldItalic.ttf');
|
||||||
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-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;
|
||||||
|
}
|
||||||
@@ -8,7 +8,7 @@ const Dashboard = () => {
|
|||||||
const { groupId, deviceId } = useParams();
|
const { groupId, deviceId } = useParams();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className='container justify-content-center'>
|
<main className='container justify-content-center gap-3 d-flex flex-column'>
|
||||||
<SummaryCards groupId={groupId} deviceId={deviceId} />
|
<SummaryCards groupId={groupId} deviceId={deviceId} />
|
||||||
<PollutionMap groupId={groupId} deviceId={deviceId} />
|
<PollutionMap groupId={groupId} deviceId={deviceId} />
|
||||||
<HistoryCharts groupId={groupId} deviceId={deviceId} />
|
<HistoryCharts groupId={groupId} deviceId={deviceId} />
|
||||||
|
|||||||
@@ -9,8 +9,10 @@ import { useEffect, useState } from "react";
|
|||||||
import { DataProvider } from "@/context/DataContext";
|
import { DataProvider } from "@/context/DataContext";
|
||||||
|
|
||||||
import { MapContainer, TileLayer, Marker } from 'react-leaflet';
|
import { MapContainer, TileLayer, Marker } from 'react-leaflet';
|
||||||
import L from 'leaflet';
|
import L, { map } from 'leaflet';
|
||||||
import 'leaflet/dist/leaflet.css';
|
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)
|
// Icono de marcador por defecto (porque Leaflet no lo carga bien en algunos setups)
|
||||||
const markerIcon = new L.Icon({
|
const markerIcon = new L.Icon({
|
||||||
@@ -19,6 +21,7 @@ const markerIcon = new L.Icon({
|
|||||||
iconAnchor: [12, 41],
|
iconAnchor: [12, 41],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
const MiniMap = ({ lat, lon }) => (
|
const MiniMap = ({ lat, lon }) => (
|
||||||
<MapContainer
|
<MapContainer
|
||||||
center={[lat, lon]}
|
center={[lat, lon]}
|
||||||
@@ -34,6 +37,11 @@ const MiniMap = ({ lat, lon }) => (
|
|||||||
</MapContainer>
|
</MapContainer>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
MiniMap.propTypes = {
|
||||||
|
lat: PropTypes.number.isRequired,
|
||||||
|
lon: PropTypes.number.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
const GroupView = () => {
|
const GroupView = () => {
|
||||||
const { groupId } = useParams();
|
const { groupId } = useParams();
|
||||||
const { config, configLoading } = useConfig();
|
const { config, configLoading } = useConfig();
|
||||||
@@ -91,21 +99,20 @@ const GroupViewContent = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<CardContainer
|
<CardContainer
|
||||||
links
|
|
||||||
cards={data.map(device => {
|
cards={data.map(device => {
|
||||||
const latest = latestData[device.deviceId];
|
const latest = latestData[device.deviceId];
|
||||||
const gpsSensor = latest?.data[0];
|
const gpsSensor = latest?.data[0];
|
||||||
const mapPreview = gpsSensor?.lat && gpsSensor?.lon
|
const mapPreview = <MiniMap lat={gpsSensor?.lat} lon={gpsSensor?.lon} />;
|
||||||
? <MiniMap lat={gpsSensor.lat} lon={gpsSensor.lon} />
|
|
||||||
: "Sin posición";
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: device.deviceName,
|
title: device.deviceName,
|
||||||
status: `ID: ${device.deviceId}`,
|
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}`,
|
to: `/groups/${groupId}/devices/${device.deviceId}`,
|
||||||
styleMode: "override",
|
className: `col-12 col-md-6 col-lg-4 ${gpsSensor == undefined ? "led" : ""}`,
|
||||||
className: "col-12 col-md-6 col-lg-4"
|
|
||||||
};
|
};
|
||||||
})}
|
})}
|
||||||
|
|
||||||
|
|||||||
@@ -60,17 +60,16 @@ const GroupsContent = ({ config }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<CardContainer
|
<CardContainer
|
||||||
links
|
|
||||||
text
|
|
||||||
cards={data.map(group => {
|
cards={data.map(group => {
|
||||||
const groupDevices = devices[group.groupId]?.data;
|
const groupDevices = devices[group.groupId]?.data;
|
||||||
const deviceCount = groupDevices?.length;
|
const deviceCount = groupDevices?.length;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: group.groupName,
|
title: group.groupName,
|
||||||
|
link: true,
|
||||||
|
text: true,
|
||||||
status: `ID: ${group.groupId}`,
|
status: `ID: ${group.groupId}`,
|
||||||
to: `/groups/${group.groupId}`,
|
to: `/groups/${group.groupId}`,
|
||||||
styleMode: "override",
|
|
||||||
content: deviceCount != null
|
content: deviceCount != null
|
||||||
? (deviceCount === 1 ? "1 dispositivo" : `${deviceCount} dispositivos`)
|
? (deviceCount === 1 ? "1 dispositivo" : `${deviceCount} dispositivos`)
|
||||||
: "Cargando dispositivos...",
|
: "Cargando dispositivos...",
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
#include <WiFi.h>
|
#include <WiFi.h>
|
||||||
#include <PubSubClient.h>
|
#include <PubSubClient.h>
|
||||||
|
|
||||||
#define SSID "iPhone de Álvaro"
|
#define SSID "DIGIFIBRA-D2ys"
|
||||||
#define WIFI_PASSWORD "alvarito123"
|
#define WIFI_PASSWORD "4EEATsyTcZ"
|
||||||
|
|
||||||
int setupWifi();
|
int WiFi_Init();
|
||||||
@@ -10,7 +10,13 @@
|
|||||||
#define GPS_ID 1
|
#define GPS_ID 1
|
||||||
#define MAX7219_ID 1
|
#define MAX7219_ID 1
|
||||||
|
|
||||||
|
#define ECO "Solo vehiculos electricos/hibridos"
|
||||||
|
#define ALL "Todo tipo de vehiculos"
|
||||||
|
|
||||||
#define DEBUG
|
#define DEBUG
|
||||||
|
|
||||||
|
#define SENSOR 0
|
||||||
|
#define ACTUATOR 1
|
||||||
|
|
||||||
extern const uint32_t DEVICE_ID;
|
extern const uint32_t DEVICE_ID;
|
||||||
extern const int GROUP_ID;
|
extern const int GROUP_ID;
|
||||||
@@ -1,14 +1,29 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "globals.hpp"
|
#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 "JsonTools.hpp"
|
||||||
#include "RestClient.hpp"
|
#include "RestClient.hpp"
|
||||||
#include "WifiConnection.hpp"
|
#include "WifiConnection.hpp"
|
||||||
#include "MqttClient.hpp"
|
#include "MqttClient.hpp"
|
||||||
|
#if DEVICE_ROLE == SENSOR
|
||||||
#include "BME280.hpp"
|
#include "BME280.hpp"
|
||||||
#include "GPS.hpp"
|
#include "GPS.hpp"
|
||||||
#include "MAX7219.hpp"
|
|
||||||
#include "MQ7v2.hpp"
|
#include "MQ7v2.hpp"
|
||||||
|
#endif
|
||||||
|
#if DEVICE_ROLE == ACTUATOR
|
||||||
|
#include "MAX7219.hpp"
|
||||||
|
#endif
|
||||||
|
|
||||||
struct TaskTimer
|
struct TaskTimer
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -20,14 +20,16 @@ void MQTT_OnReceived(char *topic, byte *payload, unsigned int length)
|
|||||||
content.concat((char)payload[i]);
|
content.concat((char)payload[i]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if DEVICE_ROLE == ACTUATOR
|
||||||
if(content == "ECO")
|
if(content == "ECO")
|
||||||
{
|
{
|
||||||
currentMessage = "Solo vehiculos electricos/hibridos";
|
currentMessage = ECO;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
currentMessage = "Todo tipo de vehiculos";
|
currentMessage = ALL;
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
void MQTT_Init(const char *MQTTServerAddress, uint16_t MQTTServerPort)
|
void MQTT_Init(const char *MQTTServerAddress, uint16_t MQTTServerPort)
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ void hueCycle(uint8_t pos)
|
|||||||
setColor(r, g, b);
|
setColor(r, g, b);
|
||||||
}
|
}
|
||||||
|
|
||||||
int setupWifi()
|
int WiFi_Init()
|
||||||
{
|
{
|
||||||
setupLED();
|
setupLED();
|
||||||
|
|
||||||
|
|||||||
@@ -1,47 +1,67 @@
|
|||||||
#include "main.hpp"
|
#include "main.hpp"
|
||||||
|
|
||||||
const uint32_t DEVICE_ID = getChipID();
|
const uint32_t DEVICE_ID = getChipID();
|
||||||
|
const String mqttId = "CUS-" + String(DEVICE_ID, HEX);
|
||||||
const int GROUP_ID = 1;
|
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 globalTimer{0, 60000};
|
||||||
TaskTimer mqttTimer{0, 5000};
|
TaskTimer mqttTimer{0, 5000};
|
||||||
|
|
||||||
|
#if DEVICE_ROLE == ACTUATOR
|
||||||
|
TaskTimer matrixTimer{0, 25};
|
||||||
|
const char *currentMessage = ALL;
|
||||||
|
extern MD_Parola display;
|
||||||
|
#endif
|
||||||
|
|
||||||
extern HTTPClient httpClient;
|
extern HTTPClient httpClient;
|
||||||
String response;
|
String response;
|
||||||
extern MD_Parola display;
|
|
||||||
|
|
||||||
|
#if DEVICE_ROLE == SENSOR
|
||||||
MQ7Data_t mq7Data;
|
MQ7Data_t mq7Data;
|
||||||
BME280Data_t bme280Data;
|
BME280Data_t bme280Data;
|
||||||
GPSData_t gpsData;
|
GPSData_t gpsData;
|
||||||
|
#endif
|
||||||
|
|
||||||
void setup()
|
void setup()
|
||||||
{
|
{
|
||||||
Serial.begin(115200);
|
Serial.begin(115200);
|
||||||
|
|
||||||
|
#ifdef DEBUG
|
||||||
Serial.println("Iniciando...");
|
Serial.println("Iniciando...");
|
||||||
|
#endif
|
||||||
|
|
||||||
setupWifi();
|
WiFi_Init();
|
||||||
MQTT_Init(MQTT_URI, MQTT_PORT);
|
MQTT_Init(MQTT_URI, MQTT_PORT);
|
||||||
|
|
||||||
BME280_Init();
|
try
|
||||||
Serial.println("Sensor BME280 inicializado");
|
{
|
||||||
GPS_Init();
|
|
||||||
Serial.println("Sensor GPS inicializado");
|
#if DEVICE_ROLE == SENSOR
|
||||||
MQ7_Init();
|
BME280_Init();
|
||||||
Serial.println("Sensor MQ7 inicializado");
|
Serial.println("Sensor BME280 inicializado");
|
||||||
MAX7219_Init();
|
GPS_Init();
|
||||||
Serial.println("Display inicializado");
|
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()
|
void loop()
|
||||||
{
|
{
|
||||||
uint32_t now = millis();
|
uint32_t now = millis();
|
||||||
|
|
||||||
|
#if DEVICE_ROLE == ACTUATOR
|
||||||
if (now - matrixTimer.lastRun >= matrixTimer.interval)
|
if (now - matrixTimer.lastRun >= matrixTimer.interval)
|
||||||
{
|
{
|
||||||
if (MAX7219_Animate())
|
if (MAX7219_Animate())
|
||||||
@@ -50,9 +70,11 @@ void loop()
|
|||||||
}
|
}
|
||||||
matrixTimer.lastRun = now;
|
matrixTimer.lastRun = now;
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
if (now - globalTimer.lastRun >= globalTimer.interval)
|
if (now - globalTimer.lastRun >= globalTimer.interval)
|
||||||
{
|
{
|
||||||
|
#if DEVICE_ROLE == SENSOR
|
||||||
readBME280();
|
readBME280();
|
||||||
readGPS();
|
readGPS();
|
||||||
readMQ7();
|
readMQ7();
|
||||||
@@ -62,17 +84,29 @@ void loop()
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
sendSensorData();
|
sendSensorData();
|
||||||
|
#endif
|
||||||
globalTimer.lastRun = now;
|
globalTimer.lastRun = now;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (now - mqttTimer.lastRun >= mqttTimer.interval)
|
if (now - mqttTimer.lastRun >= mqttTimer.interval)
|
||||||
{
|
{
|
||||||
MQTT_Handle(id.c_str());
|
MQTT_Handle(mqttId.c_str());
|
||||||
mqttTimer.lastRun = now;
|
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()
|
void readMQ7()
|
||||||
{
|
{
|
||||||
const float CO_THRESHOLD = 100.0f;
|
const float CO_THRESHOLD = 100.0f;
|
||||||
@@ -89,15 +123,6 @@ void readGPS()
|
|||||||
gpsData = GPS_Read_Fake();
|
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()
|
void printAllData()
|
||||||
{
|
{
|
||||||
Serial.println("---------------------");
|
Serial.println("---------------------");
|
||||||
@@ -130,7 +155,6 @@ void sendSensorData()
|
|||||||
{
|
{
|
||||||
const String deviceId = String(DEVICE_ID, HEX);
|
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 gpsValid = gpsData.lat != 0.0f && gpsData.lon != 0.0f;
|
||||||
bool weatherValid = bme280Data.temperature != 0.0f &&
|
bool weatherValid = bme280Data.temperature != 0.0f &&
|
||||||
bme280Data.humidity != 0.0f &&
|
bme280Data.humidity != 0.0f &&
|
||||||
@@ -156,10 +180,11 @@ void sendSensorData()
|
|||||||
postRequest(String(API_URI) + "/batch", json, response);
|
postRequest(String(API_URI) + "/batch", json, response);
|
||||||
|
|
||||||
#ifdef DEBUG
|
#ifdef DEBUG
|
||||||
Serial.println("📬 Respuesta del servidor:");
|
Serial.println("📥 Respuesta del servidor:");
|
||||||
Serial.println(response);
|
Serial.println(response);
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
uint32_t getChipID()
|
uint32_t getChipID()
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user