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 4e8cb7c..1be9221 100644 Binary files a/frontend/public/fonts/LEDBOARD.ttf and b/frontend/public/fonts/LEDBOARD.ttf differ diff --git a/frontend/src/components/layout/Card.jsx b/frontend/src/components/layout/Card.jsx index 438702e..de6eb82 100644 --- a/frontend/src/components/layout/Card.jsx +++ b/frontend/src/components/layout/Card.jsx @@ -41,7 +41,7 @@ const Card = ({ const cardContent = (

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