From fc130cc92d417ad02a0866236992afcc6fd153d3 Mon Sep 17 00:00:00 2001 From: Jose Date: Fri, 30 May 2025 19:28:12 +0200 Subject: [PATCH] F I N --- .../miarma/contaminus/common/Constants.java | 8 +- .../common/VoronoiZoneDetector.java | 50 ++++- .../miarma/contaminus/dao/ActuatorDAO.java | 5 +- .../net/miarma/contaminus/dao/DeviceDAO.java | 6 +- .../miarma/contaminus/db/DatabaseManager.java | 2 +- .../miarma/contaminus/db/QueryBuilder.java | 4 +- .../verticles/DataLayerAPIVerticle.java | 8 +- .../verticles/LogicLayerAPIVerticle.java | 212 +++++++++++++----- backend/src/main/resources/default.properties | 1 + backend/src/main/resources/openapi.yml | 8 +- frontend/public/config/settings.dev.json | 6 +- frontend/public/config/settings.prod.json | 6 +- frontend/public/fonts/LEDBOARD.ttf | Bin 172496 -> 46436 bytes frontend/src/components/layout/Card.jsx | 30 ++- frontend/src/css/Card.css | 16 +- frontend/src/pages/GroupView.jsx | 63 +++++- hardware/.vscode/settings.json | 4 +- hardware/include/JsonTools.hpp | 11 +- hardware/include/MAX7219.hpp | 6 + hardware/include/MQ7v2.hpp | 3 +- hardware/include/MqttClient.hpp | 1 + hardware/include/RestClient.hpp | 3 +- hardware/include/globals.hpp | 12 +- hardware/include/main.hpp | 2 - hardware/src/lib/http/JsonTools.cpp | 157 +++---------- hardware/src/lib/http/RestClient.cpp | 16 ++ hardware/src/lib/inet/MqttClient.cpp | 20 +- hardware/src/lib/sensor/GPS.cpp | 2 +- hardware/src/lib/sensor/MQ7v2.cpp | 18 +- hardware/src/main.cpp | 31 ++- 30 files changed, 429 insertions(+), 282 deletions(-) diff --git a/backend/src/main/java/net/miarma/contaminus/common/Constants.java b/backend/src/main/java/net/miarma/contaminus/common/Constants.java index 0dc3f57..d86d0a3 100644 --- a/backend/src/main/java/net/miarma/contaminus/common/Constants.java +++ b/backend/src/main/java/net/miarma/contaminus/common/Constants.java @@ -10,8 +10,8 @@ public class Constants { public static final String CONTAMINUS_EB = "contaminus.eventbus"; public static Logger LOGGER = LoggerFactory.getLogger(Constants.APP_NAME); - public static final int SENSOR_ROLE = 0; - public static final int ACTUATOR_ROLE = 1; + public static final Integer SENSOR_ROLE = 0; + public static final Integer ACTUATOR_ROLE = 1; /* API Endpoints */ public static final String GROUPS = RAW_API_PREFIX + "/groups"; // GET, POST @@ -33,8 +33,8 @@ public class Constants { public static final String ADD_CO_VALUE = RAW_API_PREFIX + "/groups/:groupId/devices/:deviceId/sensors/:sensorId/co_values"; // POST public static final String ACTUATORS = RAW_API_PREFIX + "/groups/:groupId/devices/:deviceId/actuators"; // GET, POST - public static final String ACTUATOR = RAW_API_PREFIX + "/groups/:groupId/devices/:deviceId/actuators/:actuator_id"; // GET, PUT - public static final String ACTUATOR_STATUS = API_PREFIX + "/groups/:groupId/devices/:deviceId/actuators/:actuator_id/status"; // GET + public static final String ACTUATOR = RAW_API_PREFIX + "/groups/:groupId/devices/:deviceId/actuators/:actuatorId"; // GET, PUT + public static final String ACTUATOR_STATUS = API_PREFIX + "/groups/:groupId/devices/:deviceId/actuators/:actuatorId/status"; // GET, PUT public static final String VIEW_LATEST_VALUES = RAW_API_PREFIX + "/v_latest_values"; // GET public static final String VIEW_POLLUTION_MAP = RAW_API_PREFIX + "/v_pollution_map"; // GET diff --git a/backend/src/main/java/net/miarma/contaminus/common/VoronoiZoneDetector.java b/backend/src/main/java/net/miarma/contaminus/common/VoronoiZoneDetector.java index e108196..e34082c 100644 --- a/backend/src/main/java/net/miarma/contaminus/common/VoronoiZoneDetector.java +++ b/backend/src/main/java/net/miarma/contaminus/common/VoronoiZoneDetector.java @@ -13,6 +13,7 @@ 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.ParseException; import org.locationtech.jts.io.geojson.GeoJsonReader; import com.google.gson.Gson; @@ -36,16 +37,21 @@ public class VoronoiZoneDetector { private static final GeometryFactory geometryFactory = new GeometryFactory(); private final Gson gson = new Gson(); - public VoronoiZoneDetector(String geojsonUrl, boolean isUrl) throws Exception { + private VoronoiZoneDetector(String geojsonUrl, boolean isUrl) { 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()); - } + try { + 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()); + } + } catch (Exception e) { + Constants.LOGGER.error("⚠️ Error al cargar el GeoJSON: " + e.getMessage()); + throw new RuntimeException("Error al cargar el GeoJSON", e); + } JsonObject root = JsonParser.parseString(geojsonStr).getAsJsonObject(); JsonArray features = root.getAsJsonArray("features"); @@ -62,7 +68,13 @@ public class VoronoiZoneDetector { JsonObject geometryJson = feature.getAsJsonObject("geometry"); String geometryStr = gson.toJson(geometryJson); - Geometry geometry = reader.read(geometryStr); + Geometry geometry = null; + try { + geometry = reader.read(geometryStr); + } catch (ParseException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } if (geometry instanceof Polygon polygon) { zones.add(new Zone(polygon, groupId)); @@ -71,8 +83,12 @@ public class VoronoiZoneDetector { } } } + + public static VoronoiZoneDetector create(String geojsonUrl, boolean isUrl) { + return new VoronoiZoneDetector(geojsonUrl, isUrl); + } - public static Integer getZoneForPoint(double lon, double lat) { + public Integer getZoneForPoint(double lon, double lat) { Point p = geometryFactory.createPoint(new Coordinate(lon, lat)); for (Zone z : zones) { @@ -83,4 +99,18 @@ public class VoronoiZoneDetector { 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; + + Integer 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/dao/ActuatorDAO.java b/backend/src/main/java/net/miarma/contaminus/dao/ActuatorDAO.java index c6a3336..c53781e 100644 --- a/backend/src/main/java/net/miarma/contaminus/dao/ActuatorDAO.java +++ b/backend/src/main/java/net/miarma/contaminus/dao/ActuatorDAO.java @@ -66,7 +66,7 @@ public class ActuatorDAO implements DataAccessObject{ return promise.future(); } - public Future getByIdAndDeviceId(Integer actuatorId, String deviceId) { + public Future getByIdAndDeviceId(Integer actuatorId, String deviceId) { Promise promise = Promise.promise(); Actuator actuator = new Actuator(); actuator.setDeviceId(deviceId); @@ -101,7 +101,8 @@ public class ActuatorDAO implements DataAccessObject{ @Override public Future update(Actuator t) { Promise promise = Promise.promise(); - String query = QueryBuilder.update(t).build(); + String query = QueryBuilder.update(t).build(); + System.out.println(); db.execute(query, Actuator.class, list -> promise.complete(list.isEmpty() ? null : list.get(0)), diff --git a/backend/src/main/java/net/miarma/contaminus/dao/DeviceDAO.java b/backend/src/main/java/net/miarma/contaminus/dao/DeviceDAO.java index 5ef87e9..b32f1bd 100644 --- a/backend/src/main/java/net/miarma/contaminus/dao/DeviceDAO.java +++ b/backend/src/main/java/net/miarma/contaminus/dao/DeviceDAO.java @@ -66,7 +66,7 @@ public class DeviceDAO implements DataAccessObject { return promise.future(); } - public Future getByIdAndGroupId(String id, Integer groupId) { + public Future getByIdAndGroupId(String id, Integer groupId) { Promise promise = Promise.promise(); Device device = new Device(); device.setDeviceId(id); @@ -75,8 +75,8 @@ public class DeviceDAO implements DataAccessObject { String query = QueryBuilder .select(Device.class) .where(device) - .build(); - + .build(); + db.execute(query, Device.class, list -> promise.complete(list.isEmpty() ? null : list.get(0)), promise::fail diff --git a/backend/src/main/java/net/miarma/contaminus/db/DatabaseManager.java b/backend/src/main/java/net/miarma/contaminus/db/DatabaseManager.java index d6370ac..103ff5c 100644 --- a/backend/src/main/java/net/miarma/contaminus/db/DatabaseManager.java +++ b/backend/src/main/java/net/miarma/contaminus/db/DatabaseManager.java @@ -48,7 +48,7 @@ public class DatabaseManager { | InvocationTargetException e) { Constants.LOGGER.error("Error instantiating class: " + e.getMessage()); } - } + } return results; }).onComplete(ar -> { if (ar.succeeded()) { diff --git a/backend/src/main/java/net/miarma/contaminus/db/QueryBuilder.java b/backend/src/main/java/net/miarma/contaminus/db/QueryBuilder.java index 30f2ec1..975e792 100644 --- a/backend/src/main/java/net/miarma/contaminus/db/QueryBuilder.java +++ b/backend/src/main/java/net/miarma/contaminus/db/QueryBuilder.java @@ -216,7 +216,7 @@ public class QueryBuilder { String fieldName = field.getName(); Object value = extractValue(fieldValue); - if (fieldName.endsWith("_id")) { + if (fieldName.endsWith("Id")) { idField = field; whereJoiner.add(fieldName + " = " + (value instanceof String || value instanceof LocalDateTime ? "'" + value + "'" : value)); @@ -258,7 +258,7 @@ public class QueryBuilder { String fieldName = field.getName(); Object fieldValue = field.get(object); - if (fieldName.endsWith("_id")) { + if (fieldName.endsWith("Id")) { idField = field; Object value = extractValue(fieldValue); whereJoiner.add(fieldName + " = " + (value instanceof String || value instanceof LocalDateTime ? "'" + value + "'" : value)); diff --git a/backend/src/main/java/net/miarma/contaminus/verticles/DataLayerAPIVerticle.java b/backend/src/main/java/net/miarma/contaminus/verticles/DataLayerAPIVerticle.java index 7fcfc4e..30152ac 100644 --- a/backend/src/main/java/net/miarma/contaminus/verticles/DataLayerAPIVerticle.java +++ b/backend/src/main/java/net/miarma/contaminus/verticles/DataLayerAPIVerticle.java @@ -418,17 +418,17 @@ public class DataLayerAPIVerticle extends AbstractVerticle { Integer groupId = Integer.parseInt(context.request().getParam("groupId")); String deviceId = context.request().getParam("deviceId"); Integer actuatorId = Integer.parseInt(context.request().getParam("actuatorId")); - + deviceDAO.getByIdAndGroupId(deviceId, groupId).compose(device -> { if (device == null) { - return Future.succeededFuture(null); - } + return Future.failedFuture(new RuntimeException("Dispositivo no encontrado")); + } return actuatorDAO.getByIdAndDeviceId(actuatorId, device.getDeviceId()); }).onSuccess(actuator -> { if (actuator == null) { context.response().setStatusCode(404).end("Actuator no encontrado"); return; - } + } context.response() .putHeader("content-type", "application/json; charset=utf-8") .end(gson.toJson(actuator)); 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 d935bee..bfb24c4 100644 --- a/backend/src/main/java/net/miarma/contaminus/verticles/LogicLayerAPIVerticle.java +++ b/backend/src/main/java/net/miarma/contaminus/verticles/LogicLayerAPIVerticle.java @@ -26,6 +26,7 @@ import io.vertx.mqtt.MqttClientOptions; import net.miarma.contaminus.common.ConfigManager; import net.miarma.contaminus.common.Constants; import net.miarma.contaminus.common.VoronoiZoneDetector; +import net.miarma.contaminus.entities.Actuator; import net.miarma.contaminus.entities.COValue; import net.miarma.contaminus.entities.Device; import net.miarma.contaminus.entities.GpsValue; @@ -41,6 +42,7 @@ public class LogicLayerAPIVerticle extends AbstractVerticle { private final Gson gson = new GsonBuilder().serializeNulls().create(); private RestClientUtil restClient; private MqttClient mqttClient; + private VoronoiZoneDetector detector; public LogicLayerAPIVerticle() { this.configManager = ConfigManager.getInstance(); @@ -53,6 +55,7 @@ public class LogicLayerAPIVerticle extends AbstractVerticle { .setUsername("contaminus") .setPassword("contaminus") ); + this.detector = VoronoiZoneDetector.create("https://miarma.net/files/voronoi_sevilla_geovoronoi.geojson", true); } @Override @@ -76,7 +79,9 @@ public class LogicLayerAPIVerticle extends AbstractVerticle { router.route(HttpMethod.GET, Constants.POLLUTION_MAP).handler(this::getDevicePollutionMap); router.route(HttpMethod.GET, Constants.HISTORY).handler(this::getDeviceHistory); router.route(HttpMethod.GET, Constants.SENSOR_VALUES).handler(this::getSensorValues); - + router.route(HttpMethod.GET, Constants.ACTUATOR_STATUS).handler(this::getActuatorStatus); + router.route(HttpMethod.POST, Constants.ACTUATOR_STATUS).handler(this::postActuatorStatus); + mqttClient.connect(1883, "localhost", ar -> { if (ar.succeeded()) { Constants.LOGGER.info("🟢 MQTT client connected"); @@ -152,80 +157,165 @@ public class LogicLayerAPIVerticle extends AbstractVerticle { private void addBatch(RoutingContext context) { JsonObject body = context.body().asJsonObject(); - if (body == null) { - context.response().setStatusCode(400).end("Missing JSON body"); - return; - } - - String groupId = body.getString("groupId"); + String groupId = body.getString("groupId"); String deviceId = body.getString("deviceId"); - JsonObject gps = body.getJsonObject("gps"); - JsonObject weather = body.getJsonObject("weather"); - JsonObject co = body.getJsonObject("co"); + JsonObject gpsJson = body.getJsonObject("gps"); + JsonObject weatherJson = body.getJsonObject("weather"); + JsonObject coJson = body.getJsonObject("co"); - if (deviceId == null || gps == null || weather == null || co == null) { - context.response().setStatusCode(400).end("Missing required fields"); + if (groupId == null || deviceId == null || gpsJson == null || weatherJson == null || coJson == null) { + sendError(context, 400, "Missing required fields"); return; } - GpsValue gpsValue = gson.fromJson(gps.toString(), GpsValue.class); - WeatherValue weatherValue = gson.fromJson(weather.toString(), WeatherValue.class); - COValue coValue = gson.fromJson(co.toString(), COValue.class); - - if(!VoronoiZoneDetector.getZoneForPoint(gpsValue.getLat(), gpsValue.getLon()) - .equals(Integer.valueOf(groupId))) { - Constants.LOGGER.info("El dispositivo no ha medido en su zona"); - return; + GpsValue gpsValue = gson.fromJson(gpsJson.toString(), GpsValue.class); + WeatherValue weatherValue = gson.fromJson(weatherJson.toString(), WeatherValue.class); + COValue coValue = gson.fromJson(coJson.toString(), COValue.class); + + if (!isInCorrectZone(gpsValue, groupId)) { + sendZoneWarning(context); + return; } - String host = "http://" + configManager.getHost(); - int port = configManager.getDataApiPort(); - String gpsPath = Constants.ADD_GPS_VALUE.replace(":groupId", groupId).replace(":deviceId", deviceId); - String weatherPath = Constants.ADD_WEATHER_VALUE.replace(":groupId", groupId).replace(":deviceId", deviceId); - String coPath = Constants.ADD_CO_VALUE.replace(":groupId", groupId).replace(":deviceId", deviceId); - String devicesPath = Constants.DEVICES.replace(":groupId", groupId); - - restClient.getRequest(port, host, devicesPath, Device[].class) - .onSuccess(ar -> { - Arrays.stream(ar) - .filter(d -> d.getDeviceRole().equals(Constants.ACTUATOR_ROLE)) - .forEach(d -> { - float coAmount = coValue.getValue(); - Constants.LOGGER.info("CO amount received: " + coAmount); - String topic = buildTopic(Integer.parseInt(groupId), d.getDeviceId(), "matrix"); - 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); - } - }); - - }) - .onFailure(err -> { - context.fail(500, err); - }); - + handleActuators(groupId, coValue.getValue()); + gpsValue.setDeviceId(deviceId); weatherValue.setDeviceId(deviceId); coValue.setDeviceId(deviceId); - restClient.postRequest(port, host, gpsPath, gpsValue, GpsValue.class) - .compose(_ -> restClient.postRequest(port, host, weatherPath, weatherValue, WeatherValue.class)) - .compose(_ -> restClient.postRequest(port, host, coPath, coValue, COValue.class)) - .onSuccess(_ -> { - context.response() - .setStatusCode(201) - .putHeader("Content-Type", "application/json") - .end(new JsonObject().put("status", "success").put("inserted", 3).encode()); - }) - .onFailure(err -> context.fail(500, err)); + storeMeasurements(context, groupId, deviceId, gpsValue, weatherValue, coValue); } - private String buildTopic(int groupId, String deviceId, String topic) - { + private void getActuatorStatus(RoutingContext context) { + String groupId = context.request().getParam("groupId"); + String deviceId = context.request().getParam("deviceId"); + String actuatorId = context.request().getParam("actuatorId"); + + String host = "http://" + configManager.getHost(); + int port = configManager.getDataApiPort(); + String actuatorPath = Constants.ACTUATOR + .replace(":groupId", groupId) + .replace(":deviceId", deviceId) + .replace(":actuatorId", actuatorId); + + restClient.getRequest(port, host, actuatorPath, Actuator.class) + .onSuccess(actuator -> { + String actuatorStatus = actuator.getStatus() == 0 ? "Solo vehiculos electricos/hibridos" : "Todo tipo de vehiculos"; + + context.response() + .setStatusCode(200) + .putHeader("Content-Type", "application/json") + .end(new JsonObject().put("status", "success").put("actuatorStatus", actuatorStatus).encode()); + }) + .onFailure(_ -> sendError(context, 500, "Failed to retrieve actuator status")); + } + + private void postActuatorStatus(RoutingContext context) { + String groupId = context.request().getParam("groupId"); + String deviceId = context.request().getParam("deviceId"); + String actuatorId = context.request().getParam("actuatorId"); + + JsonObject body = context.body().asJsonObject(); + String actuatorStatus = body.getString("status"); + + String host = "http://" + configManager.getHost(); + int port = configManager.getDataApiPort(); + String actuatorPath = Constants.ACTUATOR + .replace(":groupId", groupId) + .replace(":deviceId", deviceId) + .replace(":actuatorId", actuatorId); + + Actuator updatedActuator = new Actuator(null, null, Integer.valueOf(actuatorStatus), null); // Assuming status 1 is the desired state + + restClient.putRequest(port, host, actuatorPath, updatedActuator, Actuator.class) + .onSuccess(_ -> { + context.response() + .setStatusCode(200) + .putHeader("Content-Type", "application/json") + .end(new JsonObject().put("status", "success").put("message", "Actuator status updated").encode()); + }) + .onFailure(_ -> sendError(context, 500, "Failed to update actuator status")); + } + + private void sendError(RoutingContext ctx, int status, String msg) { + ctx.response().setStatusCode(status).end(msg); + } + + private boolean isInCorrectZone(GpsValue gps, String expectedZone) { + Integer actualZone = detector.getZoneForPoint(gps.getLon(), gps.getLat()); + Constants.LOGGER.info(gps.getLat() + ", " + gps.getLon() + " -> Zone: " + actualZone); + return actualZone.equals(Integer.valueOf(expectedZone)); + } + + private void sendZoneWarning(RoutingContext ctx) { + Constants.LOGGER.info("El dispositivo no ha medido en su zona"); + ctx.response() + .setStatusCode(200) + .putHeader("Content-Type", "application/json") + .end(new JsonObject().put("status", "success").put("message", "Device did not measure in its zone").encode()); + } + + private void handleActuators(String groupId, float coAmount) { + String host = "http://" + configManager.getHost(); + int port = configManager.getDataApiPort(); + String devicesPath = Constants.DEVICES.replace(":groupId", groupId); + + restClient.getRequest(port, host, devicesPath, Device[].class) + .onSuccess(devices -> Arrays.stream(devices) + .filter(d -> Constants.ACTUATOR_ROLE.equals(d.getDeviceRole())) + .forEach(d -> { + String topic = buildTopic(Integer.parseInt(groupId), d.getDeviceId(), "matrix"); + publishMQTT(topic, coAmount); + + String actuatorsPath = Constants.ACTUATORS + .replace(":groupId", groupId) + .replace(":deviceId", d.getDeviceId()); + + restClient.getRequest(port, host, actuatorsPath, Actuator[].class) + .onSuccess(actuators -> Arrays.stream(actuators).forEach(a -> { + String actuatorPath = Constants.ACTUATOR + .replace(":groupId", groupId) + .replace(":deviceId", d.getDeviceId()) + .replace(":actuatorId", String.valueOf(a.getActuatorId())); + Actuator updated = new Actuator(a.getActuatorId(), d.getDeviceId(), coAmount >= 80.0f ? 0 : 1, null); + restClient.putRequest(port, host, actuatorPath, updated, Actuator.class); + })) + .onFailure(err -> Constants.LOGGER.error("Failed to update actuator", err)); + })) + .onFailure(err -> Constants.LOGGER.error("Failed to retrieve devices", err)); + } + + private void publishMQTT(String topic, float coAmount) { + if (mqttClient.isConnected()) { + Constants.LOGGER.info("Publishing to MQTT topic: " + topic); + mqttClient.publish(topic, Buffer.buffer(coAmount >= 80.0f ? "ECO" : "GAS"), + MqttQoS.AT_LEAST_ONCE, false, false); + } + } + + private void storeMeasurements(RoutingContext ctx, String groupId, String deviceId, + GpsValue gps, WeatherValue weather, COValue co) { + + String host = "http://" + configManager.getHost(); + int port = configManager.getDataApiPort(); + + String gpsPath = Constants.ADD_GPS_VALUE.replace(":groupId", groupId).replace(":deviceId", deviceId); + String weatherPath = Constants.ADD_WEATHER_VALUE.replace(":groupId", groupId).replace(":deviceId", deviceId); + String coPath = Constants.ADD_CO_VALUE.replace(":groupId", groupId).replace(":deviceId", deviceId); + + restClient.postRequest(port, host, gpsPath, gps, GpsValue.class) + .compose(_ -> restClient.postRequest(port, host, weatherPath, weather, WeatherValue.class)) + .compose(_ -> restClient.postRequest(port, host, coPath, co, COValue.class)) + .onSuccess(_ -> ctx.response() + .setStatusCode(201) + .putHeader("Content-Type", "application/json") + .end(new JsonObject().put("status", "success").put("inserted", 3).encode())) + .onFailure(err -> ctx.fail(500, err)); + } + + + private String buildTopic(int groupId, String deviceId, String topic) { String topicString = "group/" + groupId + "/device/" + deviceId + "/" + topic; return topicString; } diff --git a/backend/src/main/resources/default.properties b/backend/src/main/resources/default.properties index 5cb15b1..e797ca3 100644 --- a/backend/src/main/resources/default.properties +++ b/backend/src/main/resources/default.properties @@ -9,6 +9,7 @@ dp.poolSize=5 # HTTP Server Configuration inet.host=localhost +mqtt.host=localhost webserver.port=8080 data-api.port=8081 logic-api.port=8082 \ No newline at end of file diff --git a/backend/src/main/resources/openapi.yml b/backend/src/main/resources/openapi.yml index d251ecd..325ac19 100644 --- a/backend/src/main/resources/openapi.yml +++ b/backend/src/main/resources/openapi.yml @@ -183,7 +183,7 @@ paths: responses: "200": description: Operación exitosa - /api/raw/v1/groups/{groupId}/devices/{deviceId}/actuators/{actuator_id}: + /api/raw/v1/groups/{groupId}/devices/{deviceId}/actuators/{actuatorId}: get: summary: Información de un actuador parameters: @@ -197,7 +197,7 @@ paths: required: true schema: type: string - - name: actuator_id + - name: actuatorId in: path required: true schema: @@ -205,7 +205,7 @@ paths: responses: "200": description: Operación exitosa - /api/v1/groups/{groupId}/devices/{deviceId}/actuators/{actuator_id}/status: + /api/v1/groups/{groupId}/devices/{deviceId}/actuators/{actuatorId}/status: get: summary: Estado de un actuador parameters: @@ -219,7 +219,7 @@ paths: required: true schema: type: string - - name: actuator_id + - name: actuatorId in: path required: true schema: diff --git a/frontend/public/config/settings.dev.json b/frontend/public/config/settings.dev.json index 8029fda..13642d0 100644 --- a/frontend/public/config/settings.dev.json +++ b/frontend/public/config/settings.dev.json @@ -28,10 +28,10 @@ "GET_SENSOR_VALUES": "/groups/:groupId/devices/:deviceId/sensors/:sensorId/values", "GET_ACTUATORS": "/groups/:groupId/devices/:deviceId/actuators", - "GET_ACTUATOR_BY_ID": "/groups/:groupId/devices/:deviceId/actuators/:actuator_id", + "GET_ACTUATOR_BY_ID": "/groups/:groupId/devices/:deviceId/actuators/:actuatorId", "POST_ACTUATORS": "/groups/:groupId/devices/:deviceId/actuators", - "PUT_ACTUATOR_BY_ID": "/groups/:groupId/devices/:deviceId/actuators/:actuator_id", - "GET_ACTUATOR_STATUS": "/groups/:groupId/devices/:deviceId/actuators/:actuator_id/status", + "PUT_ACTUATOR_BY_ID": "/groups/:groupId/devices/:deviceId/actuators/:actuatorId", + "GET_ACTUATOR_STATUS": "/groups/:groupId/devices/:deviceId/actuators/:actuatorId/status", "VIEW_LATEST_VALUES": "/v_latest_values", "VIEW_POLLUTION_MAP": "/v_pollution_map", diff --git a/frontend/public/config/settings.prod.json b/frontend/public/config/settings.prod.json index fc3c250..2e7e607 100644 --- a/frontend/public/config/settings.prod.json +++ b/frontend/public/config/settings.prod.json @@ -28,10 +28,10 @@ "GET_SENSOR_VALUES": "/groups/:groupId/devices/:deviceId/sensors/:sensorId/values", "GET_ACTUATORS": "/groups/:groupId/devices/:deviceId/actuators", - "GET_ACTUATOR_BY_ID": "/groups/:groupId/devices/:deviceId/actuators/:actuator_id", + "GET_ACTUATOR_BY_ID": "/groups/:groupId/devices/:deviceId/actuators/:actuatorId", "POST_ACTUATORS": "/groups/:groupId/devices/:deviceId/actuators", - "PUT_ACTUATOR_BY_ID": "/groups/:groupId/devices/:deviceId/actuators/:actuator_id", - "GET_ACTUATOR_STATUS": "/groups/:groupId/devices/:deviceId/actuators/:actuator_id/status", + "PUT_ACTUATOR_BY_ID": "/groups/:groupId/devices/:deviceId/actuators/:actuatorId", + "GET_ACTUATOR_STATUS": "/groups/:groupId/devices/:deviceId/actuators/:actuatorId/status", "VIEW_LATEST_VALUES": "/v_latest_values", "VIEW_POLLUTION_MAP": "/v_pollution_map", diff --git a/frontend/public/fonts/LEDBOARD.ttf b/frontend/public/fonts/LEDBOARD.ttf index 4e8cb7c69b0ed1ce0aab349a20c5144f02216e6f..1be9221bcf11a451ef51613f108429373404acc2 100644 GIT binary patch literal 46436 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

@@ -49,18 +49,24 @@ const Card = ({ {shortTitle}

-
- {marquee ? ( - -

{children}

-
- ) : text ? ( -

{children}

- ) : ( -
{children}
- )} -
+ {marquee && ( +
+ +

{children}

+
+
+ )} + + {!marquee && ( +
+ {text ? ( +

{children}

+ ) : ( + <>{children} + )} +
+ )} {status && {status}} diff --git a/frontend/src/css/Card.css b/frontend/src/css/Card.css index eae7d0e..7633a17 100644 --- a/frontend/src/css/Card.css +++ b/frontend/src/css/Card.css @@ -14,7 +14,8 @@ } .card.light > span.status { - background: #E0E0E0; + background: #D0D0D0; + color: #505050; } .card.dark { @@ -27,6 +28,7 @@ .card.dark > span.status { background: #505050; + color: #D0D0D0; } .card:hover { @@ -48,12 +50,20 @@ .card.led marquee > p.card-text { font-family: "LEDBOARD" !important; - color: rgb(38, 60, 229) !important; - font-size: 2.5em !important; + color: #263ce5 !important; + font-size: 6em !important; text-transform: uppercase !important; letter-spacing: 1px !important; } +.contenedor-con-efecto { + background-color: #000; /* Fondo oscuro, similar al de la imagen */ + background-image: radial-gradient(#313131aa 3px, transparent 0.5px); /* Puntos muy sutiles, casi invisibles */ + background-size: 8px 8px; /* Controla la separación de los puntos */ + /* Ajusta estos valores para cambiar el tamaño y la densidad de los puntos */ + /* Los colores de los puntos pueden ser un azul muy oscuro, casi negro, o el mismo negro del fondo para un efecto muy sutil. */ +} + p.card-text { font-size: 2.2em; font-weight: 600; diff --git a/frontend/src/pages/GroupView.jsx b/frontend/src/pages/GroupView.jsx index f686909..9b2b1ae 100644 --- a/frontend/src/pages/GroupView.jsx +++ b/frontend/src/pages/GroupView.jsx @@ -9,10 +9,9 @@ import { useEffect, useState } from "react"; import { DataProvider } from "@/context/DataContext"; import { MapContainer, TileLayer, Marker } from 'react-leaflet'; -import L, { map } from 'leaflet'; +import L 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({ @@ -24,13 +23,15 @@ const markerIcon = new L.Icon({ const MiniMap = ({ lat, lon }) => ( @@ -65,9 +66,11 @@ const GroupViewContent = () => { const { data, dataLoading, dataError, getData } = useDataContext(); const { groupId } = useParams(); const [latestData, setLatestData] = useState({}); + const [actuatorStatus, setActuatorStatus] = useState({}); const { config } = useConfig(); // lo pillamos por si acaso const latestValuesUrl = config.appConfig.endpoints.LOGIC_URL + config.appConfig.endpoints.GET_DEVICE_LATEST_VALUES; + const actuatorStatusUrl = config.appConfig.endpoints.LOGIC_URL + config.appConfig.endpoints.GET_ACTUATOR_STATUS; useEffect(() => { if (!data || data.length === 0) return; @@ -94,6 +97,52 @@ const GroupViewContent = () => { fetchLatestData(); }, [data, groupId]); + useEffect(() => { + if (!data || data.length === 0) return; + + const fetchActuatorStatus = async () => { + const statusByDevice = {}; + + await Promise.all(data.map(async device => { + const actuatorListUrl = config.appConfig.endpoints.DATA_URL + + config.appConfig.endpoints.GET_ACTUATORS + .replace(':groupId', groupId) + .replace(':deviceId', device.deviceId); + + try { + const response = await getData(actuatorListUrl); + const actuators = Array.isArray(response?.data) ? response.data : []; + + const statuses = await Promise.all( + actuators.map(async (actuator) => { + const statusUrl = actuatorStatusUrl + .replace(':groupId', groupId) + .replace(':deviceId', device.deviceId) + .replace(':actuatorId', actuator.actuatorId); + + try { + const status = await getData(statusUrl); // { status: ... } + return { actuatorId: actuator.actuatorId, status }; + } catch (err) { + console.error(`Error al obtener status del actuator ${actuator.actuatorId}:`, err); + return { actuatorId: actuator.actuatorId, status: null }; + } + }) + ); + + statusByDevice[device.deviceId] = statuses; + } catch (err) { + console.error(`Error al obtener actuadores de ${device.deviceId}:`, err); + statusByDevice[device.deviceId] = []; + } + })); + + setActuatorStatus(statusByDevice); + }; + + fetchActuatorStatus(); + }, [data, groupId]); + if (dataLoading) return

; if (dataError) return

Error al cargar datos: {dataError}

; @@ -104,13 +153,19 @@ const GroupViewContent = () => { const gpsSensor = latest?.data[0]; const mapPreview = ; + const actuatorById = actuatorStatus[device.deviceId] || []; + const firstStatus = actuatorById.length > 0 ? actuatorById[0].status : null; + return { title: device.deviceName, status: `ID: ${device.deviceId}`, link: gpsSensor != undefined, text: gpsSensor == undefined, marquee: gpsSensor == undefined, - content: gpsSensor == undefined ? "TODO TIPO DE VEHICULOS" : mapPreview, + + content: gpsSensor == undefined + ? (firstStatus?.data?.actuatorStatus || 'Sin estado') + : mapPreview, to: `/groups/${groupId}/devices/${device.deviceId}`, className: `col-12 col-md-6 col-lg-4 ${gpsSensor == undefined ? "led" : ""}`, }; diff --git a/hardware/.vscode/settings.json b/hardware/.vscode/settings.json index 49a210c..5006121 100644 --- a/hardware/.vscode/settings.json +++ b/hardware/.vscode/settings.json @@ -16,7 +16,9 @@ "string_view": "cpp", "initializer_list": "cpp", "system_error": "cpp", - "cmath": "cpp" + "cmath": "cpp", + "random": "cpp", + "limits": "cpp" }, "github.copilot.enable": { "*": true, diff --git a/hardware/include/JsonTools.hpp b/hardware/include/JsonTools.hpp index ad680ac..2e07621 100644 --- a/hardware/include/JsonTools.hpp +++ b/hardware/include/JsonTools.hpp @@ -5,6 +5,7 @@ #include "BME280.hpp" #include "MQ7v2.hpp" #include "GPS.hpp" +#include "MAX7219.hpp" String serializeSensorValue( int groupId, @@ -16,12 +17,4 @@ String serializeSensorValue( const MQ7Data_t &mq7, const GPSData_t &gps); -String serializeActuatorStatus( - int actuatorId, - const String &deviceId, - int status, - long timestamp); - -void deserializeSensorValue(HTTPClient &http, int httpResponseCode); -void deserializeActuatorStatus(HTTPClient &http, int httpResponseCode); -void deserializeDevice(HTTPClient &http, int httpResponseCode); \ No newline at end of file +MAX7219Status_t deserializeActuatorStatus(HTTPClient &http, int httpResponseCode); diff --git a/hardware/include/MAX7219.hpp b/hardware/include/MAX7219.hpp index 13dfb4d..040e744 100644 --- a/hardware/include/MAX7219.hpp +++ b/hardware/include/MAX7219.hpp @@ -10,6 +10,12 @@ #define CS_PIN 18 #define CLK_PIN 17 +struct MAX7219Status_t +{ + String status; + String actuatorStatus; +}; + void MAX7219_Init(); void MAX7219_DisplayText(const char *text, textPosition_t align, uint16_t speed, uint16_t pause); bool MAX7219_Animate(); diff --git a/hardware/include/MQ7v2.hpp b/hardware/include/MQ7v2.hpp index 5ced4c0..49f8a1b 100644 --- a/hardware/include/MQ7v2.hpp +++ b/hardware/include/MQ7v2.hpp @@ -14,4 +14,5 @@ struct MQ7Data_t }; void MQ7_Init(); -MQ7Data_t MQ7_Read(); \ No newline at end of file +MQ7Data_t MQ7_Read(); +MQ7Data_t MQ7_Read_Fake(); \ No newline at end of file diff --git a/hardware/include/MqttClient.hpp b/hardware/include/MqttClient.hpp index 9a4a155..2e877d2 100644 --- a/hardware/include/MqttClient.hpp +++ b/hardware/include/MqttClient.hpp @@ -3,6 +3,7 @@ #include "globals.hpp" #include #include +#include "RestClient.hpp" #define USER "contaminus" #define MQTT_PASSWORD "contaminus" diff --git a/hardware/include/RestClient.hpp b/hardware/include/RestClient.hpp index b63bb3e..66dc164 100644 --- a/hardware/include/RestClient.hpp +++ b/hardware/include/RestClient.hpp @@ -3,4 +3,5 @@ #include void getRequest(const String url, String &response); -void postRequest(const String url, const String &payload, String &response); \ No newline at end of file +void postRequest(const String url, const String &payload, String &response); +void putRequest(const String url, const String &payload, String &response); \ No newline at end of file diff --git a/hardware/include/globals.hpp b/hardware/include/globals.hpp index 15968d5..876bcdb 100644 --- a/hardware/include/globals.hpp +++ b/hardware/include/globals.hpp @@ -1,5 +1,7 @@ #include +#define DEVICE_ROLE ACTUATOR // se cambia entre SENSOR y ACTUATOR + #define MQTT_URI "miarma.net" #define API_URI "https://contaminus.miarma.net/api/v1/" #define REST_PORT 443 @@ -10,13 +12,11 @@ #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 +#define DEBUG + extern const uint32_t DEVICE_ID; -extern const int GROUP_ID; \ No newline at end of file +extern const int GROUP_ID; +extern String currentMessage; \ No newline at end of file diff --git a/hardware/include/main.hpp b/hardware/include/main.hpp index 7fa4211..aefb7c2 100644 --- a/hardware/include/main.hpp +++ b/hardware/include/main.hpp @@ -2,8 +2,6 @@ #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 diff --git a/hardware/src/lib/http/JsonTools.cpp b/hardware/src/lib/http/JsonTools.cpp index 090ef82..7d8c2c9 100644 --- a/hardware/src/lib/http/JsonTools.cpp +++ b/hardware/src/lib/http/JsonTools.cpp @@ -36,158 +36,53 @@ String serializeSensorValue( return output; } -String serializeActuatorStatus(const int actuatorId, const String &deviceId, const int status, const long timestamp) -{ - DynamicJsonDocument doc(512); - - doc["actuatorId"] = actuatorId; - doc["deviceId"] = deviceId; - doc["status"] = status; - doc["timestamp"] = timestamp; - - String output; - serializeJson(doc, output); - Serial.println(output); - return output; -} - -String serializeDevice(const String &deviceId, int groupId, const String &deviceName) -{ - DynamicJsonDocument doc(512); - - doc["deviceId"] = deviceId; - doc["groupId"] = groupId; - doc["deviceName"] = deviceName; - - String output; - serializeJson(doc, output); - Serial.println(output); - return output; -} - -void deserializeSensorValue(HTTPClient &http, int httpResponseCode) +MAX7219Status_t deserializeActuatorStatus(HTTPClient &http, int httpResponseCode) { if (httpResponseCode > 0) { +#ifdef DEBUG Serial.print("HTTP Response code: "); Serial.println(httpResponseCode); - String responseJson = http.getString(); +#endif - DynamicJsonDocument doc(ESP.getMaxAllocHeap()); - DeserializationError error = deserializeJson(doc, responseJson); - - if (error) - { - Serial.print(F("deserializeJson() failed: ")); - Serial.println(error.f_str()); - return; - } - - String groupId = doc["groupId"]; - String deviceId = doc["deviceId"]; - - JsonObject gps = doc["gps"]; - int gpsId = gps["sensorId"]; - float lat = gps["lat"]; - float lon = gps["lon"]; - - JsonObject weather = doc["weather"]; - int weatherId = weather["sensorId"]; - float temp = weather["temperature"]; - float hum = weather["humidity"]; - float pres = weather["pressure"]; - - JsonObject co = doc["co"]; - int coId = co["sensorId"]; - float coVal = co["value"]; - - Serial.println("🛰 GPS:"); - Serial.printf(" Sensor ID: %d\n Lat: %.6f Lon: %.6f\n", gpsId, lat, lon); - - Serial.println("🌤 Weather:"); - Serial.printf(" Sensor ID: %d\n Temp: %.2f°C Hum: %.2f%% Pressure: %.2f hPa\n", weatherId, temp, hum, pres); - - Serial.println("🧪 CO:"); - Serial.printf(" Sensor ID: %d\n CO: %.2f ppm\n", coId, coVal); - - Serial.printf("🧾 Group ID: %s\n", groupId.c_str()); - Serial.printf("🧾 Device ID: %s\n", deviceId.c_str()); - } - else - { - Serial.print("Error code: "); - Serial.println(httpResponseCode); - } -} - -void deserializeActuatorStatus(HTTPClient &http, int httpResponseCode) -{ - if (httpResponseCode > 0) - { - Serial.print("HTTP Response code: "); - Serial.println(httpResponseCode); String responseJson = http.getString(); DynamicJsonDocument doc(ESP.getMaxAllocHeap()); DeserializationError error = deserializeJson(doc, responseJson); if (error) { +#ifdef DEBUG Serial.print(F("deserializeJson() failed: ")); Serial.println(error.f_str()); - return; +#endif + return { + .status = "error", + .actuatorStatus = "Error" + }; } - JsonArray array = doc.as(); - for (JsonObject actuator : array) - { - int actuatorId = actuator["actuatorId"]; - String deviceId = actuator["deviceId"]; - int status = actuator["status"]; - long timestamp = actuator["timestamp"]; + String status = doc["status"] | "error"; + String actuatorStatus = doc["actuatorStatus"] | "Unknown"; - Serial.println("Actuator deserialized:"); - Serial.printf(" ID: %d\n Device: %s\n Status: %d\n Time: %ld\n\n", - actuatorId, deviceId.c_str(), status, timestamp); - } +#ifdef DEBUG + Serial.println("Actuator status deserialized:"); + Serial.printf(" Status: %s\n Actuator Status: %s\n\n", status.c_str(), actuatorStatus.c_str()); +#endif + + return { + .status = status, + .actuatorStatus = actuatorStatus + }; } else { +#ifdef DEBUG Serial.print("Error code: "); Serial.println(httpResponseCode); +#endif + return { + .status = "error", + .actuatorStatus = "HTTP error" + }; } } - -void deserializeDevice(HTTPClient &http, int httpResponseCode) -{ - if (httpResponseCode > 0) - { - Serial.print("HTTP Response code: "); - Serial.println(httpResponseCode); - String responseJson = http.getString(); - DynamicJsonDocument doc(ESP.getMaxAllocHeap()); - DeserializationError error = deserializeJson(doc, responseJson); - - if (error) - { - Serial.print(F("deserializeJson() failed: ")); - Serial.println(error.f_str()); - return; - } - - JsonArray array = doc.as(); - for (JsonObject device : array) - { - String deviceId = device["deviceId"]; - int groupId = device["groupId"]; - String deviceName = device["deviceName"]; - - Serial.println("Device deserialized:"); - Serial.printf(" ID: %s\n Group: %d\n Name: %s\n\n", deviceId.c_str(), groupId, deviceName.c_str()); - } - } - else - { - Serial.print("Error code: "); - Serial.println(httpResponseCode); - } -} \ No newline at end of file diff --git a/hardware/src/lib/http/RestClient.cpp b/hardware/src/lib/http/RestClient.cpp index e623529..b1399e2 100644 --- a/hardware/src/lib/http/RestClient.cpp +++ b/hardware/src/lib/http/RestClient.cpp @@ -31,4 +31,20 @@ void postRequest(const String url, const String &payload, String &response) response = "Error: " + String(httpCode); } httpClient.end(); +} + +void putRequest(const String url, const String &payload, String &response) +{ + httpClient.begin(url); + httpClient.addHeader("Content-Type", "application/json"); + int httpCode = httpClient.PUT(payload); + if (httpCode > 0) + { + response = httpClient.getString(); + } + else + { + response = "Error: " + String(httpCode); + } + httpClient.end(); } \ No newline at end of file diff --git a/hardware/src/lib/inet/MqttClient.cpp b/hardware/src/lib/inet/MqttClient.cpp index a1982ce..2f68eb2 100644 --- a/hardware/src/lib/inet/MqttClient.cpp +++ b/hardware/src/lib/inet/MqttClient.cpp @@ -1,7 +1,7 @@ #include "MqttClient.hpp" extern WiFiClient wifiClient; -extern const char *currentMessage; +extern HTTPClient httpClient; PubSubClient client(wifiClient); @@ -20,14 +20,20 @@ void MQTT_OnReceived(char *topic, byte *payload, unsigned int length) content.concat((char)payload[i]); } + content.trim(); + +#ifdef DEBUG + Serial.println(content); +#endif + #if DEVICE_ROLE == ACTUATOR - if(content == "ECO") + if (content.equals("ECO")) { - currentMessage = ECO; - } - else + currentMessage = "Vehiculos electricos/hibridos"; + } + else if (content.equals("GAS")) { - currentMessage = ALL; + currentMessage = "Todo tipo de vehiculos"; } #endif } @@ -67,7 +73,7 @@ void MQTT_Handle(const char *MQTTClientName) client.loop(); } -String buildTopic(int groupId, const String& deviceId, const String& topic) +String buildTopic(int groupId, const String &deviceId, const String &topic) { String topicString = "group/" + String(groupId) + "/device/" + deviceId + "/" + topic; return topicString; diff --git a/hardware/src/lib/sensor/GPS.cpp b/hardware/src/lib/sensor/GPS.cpp index 76f6d7c..ce610cb 100644 --- a/hardware/src/lib/sensor/GPS.cpp +++ b/hardware/src/lib/sensor/GPS.cpp @@ -29,5 +29,5 @@ GPSData_t GPS_Read() GPSData_t GPS_Read_Fake() { float rnd = random(-0.0005, 0.0005); - return {37.358201f + rnd, -5.986640f + rnd}; + return {37.326371f + rnd, -5.966001f + rnd}; } \ No newline at end of file diff --git a/hardware/src/lib/sensor/MQ7v2.cpp b/hardware/src/lib/sensor/MQ7v2.cpp index 89475b5..9a4e03b 100644 --- a/hardware/src/lib/sensor/MQ7v2.cpp +++ b/hardware/src/lib/sensor/MQ7v2.cpp @@ -17,4 +17,20 @@ MQ7Data_t MQ7_Read() bool d0 = digitalRead(MQ7_D0); return {ppm, d0}; -} \ No newline at end of file +} + +MQ7Data_t MQ7_Read_Fake() +{ + float ppm; + bool d0; + + if (random(0, 100) < 50) { + ppm = random(80, 500); // valores entre 101 y 500 ppm + d0 = true; + } else { + ppm = random(10, 79); // valores entre 10 y 99 ppm + d0 = false; + } + + return {ppm, d0}; +} diff --git a/hardware/src/main.cpp b/hardware/src/main.cpp index 4dbce40..209855e 100644 --- a/hardware/src/main.cpp +++ b/hardware/src/main.cpp @@ -9,7 +9,9 @@ TaskTimer mqttTimer{0, 5000}; #if DEVICE_ROLE == ACTUATOR TaskTimer matrixTimer{0, 25}; -const char *currentMessage = ALL; +TaskTimer displayTimer{0, 1000}; +String currentMessage = ""; +String lastMessage = ""; extern MD_Parola display; #endif @@ -35,7 +37,7 @@ void setup() try { - + #if DEVICE_ROLE == SENSOR BME280_Init(); Serial.println("Sensor BME280 inicializado"); @@ -48,7 +50,12 @@ void setup() #if DEVICE_ROLE == ACTUATOR MAX7219_Init(); Serial.println("Display inicializado"); - writeMatrix(currentMessage); + writeMatrix(currentMessage.c_str()); + + String url = String(API_URI) + "groups/" + GROUP_ID + "/devices/" + String(DEVICE_ID, HEX) + "/actuators/" + MAX7219_ID + "/status"; + getRequest(url, response); + MAX7219Status_t statusData = deserializeActuatorStatus(httpClient, httpClient.GET()); + currentMessage = statusData.actuatorStatus; #endif } catch (const char *e) @@ -70,6 +77,19 @@ void loop() } matrixTimer.lastRun = now; } + + if (now - displayTimer.lastRun >= displayTimer.interval) + { + if (currentMessage != lastMessage) + { + writeMatrix(currentMessage.c_str()); + lastMessage = currentMessage; +#ifdef DEBUG + Serial.println("Display actualizado"); +#endif + } + displayTimer.lastRun = now; + } #endif if (now - globalTimer.lastRun >= globalTimer.interval) @@ -99,9 +119,8 @@ void loop() void writeMatrix(const char *message) { #ifdef DEBUG - Serial.println("Escribiendo en el display..."); + Serial.println("Escribiendo mensaje: "); Serial.print(message); #endif - MAX7219_DisplayText(message, PA_LEFT, 50, 0); } #endif @@ -110,7 +129,7 @@ void writeMatrix(const char *message) void readMQ7() { const float CO_THRESHOLD = 100.0f; - mq7Data = MQ7_Read(); + mq7Data = MQ7_Read_Fake(); } void readBME280()