Plotting using NodeMCU ESP8285

Luego de Flashear Esp8266, fucK AT-commands si que seria interesante darle otro uso que solo prender o apagar un simple led.

Que tal si graficamos la temperatura y humedad con ¿el? así como cualquier otro sensor que reaccione a esos cambios ambientales.

Si que necesitamos que él corra el siguiente código. para habilitar los enpoints que consumiremos con WebClient de Spring WebFlux (el cliente reactivo de spring construido sobre project reactor y reactor-netty).

La animación siguiente muestra un Grid y un Chart donde se grafican nuestros dos sensores en tiempo real, si que toca darle créditos a @Detlef Boehm por los tips a la hora de graficar.

Este código es javascript formateado en java, se ejecutara del lado del cliente, necesario para dar una resolución de 1 segundo a la gráfica, sin el no sirve, y ni formatea los String( la hora que sale en la parte inferior de la gráfica)

private static final String FORMATTER_TIMESTAMP = "function (value, timestamp) {\n" +
        "  var date = new Date(timestamp);" +
        "  var hours = date.getHours();\n" +
        "  var minutes = date.getMinutes();\n" +
        "  var seconds = date.getSeconds();" +
        "  var ampm = hours >= 12 ? "pm" : "am";\n" +
        "  hours = hours % 12;\n" +
        "  hours = hours ? hours : 12; // the hour "0" should be "12"\n" +
        "  minutes = minutes < 10 ? "0"+minutes : minutes;\n" +
        "  return hours + ":" + minutes + ":" + ("0"+seconds).slice(-2) + " " + ampm;\n" +
        "}";

Configuración del conector de reactor-netty

Con WebClient tenemos una configuracion por defecto con la URL base que usamos en dos lugares uno para consumir el endpoint de los sensores y el otro para encender el led.

WebClient usa internamente una conexión por defecto con reactor netty, entonces cuando no tengamos respuesta de nuestro micro, el error se muestra al cliente luego de esperar 30 segundos Info - Response-timeout.

Para notificar al cliente más rápido (a los 2 segundos) nuestro bean quedará de la siguiente manera:

@Bean
public WebClient sensorWebClientBuilder(final WebClient.Builder webClient) {
    HttpClient httpClient = HttpClient.create() (1)
            .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 2000) (2)
            .doOnConnected((Connection connection) ->
                (3)
                connection.addHandlerLast(new ReadTimeoutHandler(2000, TimeUnit.MILLISECONDS)));
    return webClient
         .clientConnector(new ReactorClientHttpConnector(httpClient)) (4)
            .baseUrl(BASE_URL)
            .build();
}
1 Se configura una instancia del cliente reactor netty
2 Timeout de 2 segundos
3 ReadTimeout con 2 segundos como mínimo.
4 Se establece el cliente de reactor netty en el WebClient

La configuración anterior se pudo realizar también via properties.


Endpoint de los sensores

this.webClient
    .get()
    .uri(DHT_22)
    .accept(MediaType.APPLICATION_JSON)
    .retrieve()
    .bodyToMono(SensorDht22Mapper.class)
    .delaySubscription(Duration.ofSeconds(2)) (1)
    .repeat()
    .doOnError((Throwable throwable) -> { (2)
        log.warn("Error {}", throwable);
        ui.access(() -> this.showError(throwable.getMessage()));
    })
    .retryWhen(Retry.indefinitely()); (3)
1 Si que debemos hacer un delay de 2 segundos y repetir la suscripción para obtener los datos actualizados.
2 En caso de error notificar al usuario gracias al uso del operador doOnError.
3 Nos resuscribirnos con retryWhen indefinidamente en caso de error, intesante podria ser tambien el uso de Resilence4j que se sabe que es un api mas avanzada para la alta disponibilidad.

Endpoint del led

this.webClient
    .put()
    .uri(functionQueryParameters) (1)
    .accept(MediaType.APPLICATION_JSON)
    .retrieve()
    .bodyToMono(SensorDht22.class)
    .doOnError(error -> ui.access(() -> Notification.show(error.getMessage()))) (2)
    .subscribe(sensorDht22 -> {
        ui.access(() -> {
            Notification.show(sensorDht22.getStatus());
        });
    });
1 Es la función que nos permite crear la query por medio de un builder, hacemos un put con las query-parameters necesarias ?id=2&status={on/off}.
2 Si hay error notificamos al cliente.

Vista principal de los sensores 🔥

plotESP8285

Notificar si el micro se cae.

Simulando que el micro no este disponible, lo hacemos quitandole la tensión y después de 2 segundos sin respuesta, el backend desde Vaadin notificara al cliente.

reconnetESP8285

Usando bulkhead

Dependiendo el servidor que se use, se debe customizar también el manejo de threads, bien sea tomcat, netty etc, de manera que se efectue bien el control de hilos en conjunto con este patrón.

El patrón bulkhead bastante útil, para controlar el máximo de llamada concurrentes a la misma vez.

Muy relacionado con Little’s law, no solamente se trata de aislar el thread-pool, sino es más sobre el control del consumo/agotamiento de recursos.

L = λ * W

  • L El número promedio de tareas simultáneas en un sistema de colas.

  • λ El número promedio de tareas que llegan a un sistema de cola por unidad de tiempo.

  • W El tiempo promedio de servicio que una tarea pasa en un sistema de colas.

Dependencia de maven

<dependency>
    <groupId>io.github.resilience4j</groupId>
    <artifactId>resilience4j-bulkhead</artifactId>
    <version>1.7.1</version>
</dependency>
@Bean
public Bulkhead getBulkHeadConfig() {
    return Bulkhead.of("bulk-head", BulkheadConfig
            .custom()
            .writableStackTraceEnabled(true)
            .maxConcurrentCalls(5) (1)
            .maxWaitDuration(Duration.ofMillis(5))
            .build());
}
1 Maximo de llamadas concurrentes, pueden ser calculadas también en base a los procesadores del sistema y dividir entre 2, Runtime.getRuntime().availableProcessors() / 2;
final Flux<SensorDht22Mapper> sensorResponseMapper = this.webClient
        .get()
        .uri(DHT_22)
        .accept(MediaType.APPLICATION_JSON)
        .retrieve()
        .bodyToMono(SensorDht22Mapper.class)
        .delaySubscription(Duration.ofSeconds(2))
        .repeat()
        .transform(BulkheadOperator.of(this.bulkhead)) (1)
        .subscribeOn(Schedulers.immediate())
        .publishOn(Schedulers.immediate())
        .doOnError((Throwable throwable) -> {
            log.warn("Error {}", throwable);
            ui.access(() -> this.showError("ESP8266 not found!"));
        })
        .retryWhen(Retry.indefinitely()); (2)
1 Uso del operator transform que permite actuar en este publisher y customizarle, actuando en tiempo de ensamblado, permitiendo trabajar con el operador BulkheadOperator.
2 Este retry quizas no sea la mejor manera de hacerlo, posiblemente se le deba dar un retoque, pero nada mal.

  • Esta pendiente exportar los datos del grid a otros formatos, pdf-excell-txt-csv etc…​

  • source