Llevo varios años usando MySensors con HomeAssistant y he detectado (pocas veces) pérdidas de mensajes. Para solucionarlo MySensors soporta el uso de ACK, pero en MySensors es extraño, y hay muchos hilos de discusión muy confusos en el foro al respecto. De hecho, parece que en la última versión se han decidido mejorar las librerías para evitar la confusión.
La descripción del problema es el siguiente: algunas funciones como send()
tiene un parámetro opcional ack
que no corresponde con el ack hardware. Para distinguir entre los dos tipos de ack, se ha renombrado este parámetro a echo
debida a la confusión. Las diferencias se pueden resumir como:
-
El ack hardware solo indica que el mensaje ha llegado a otro nodo, pudiendo ser este nodo un repetidor.
-
El echo es un mensaje devuelto por el gateway con el contenido exacto que recibió el gateway, pero marcado como echo.
Para que la receta que muestro funcione, se debe revisar la instalación de la librería de MySensors que se esté usando en la instalación de Arduino, ya que puede que esté la versión con el ACK renombrado a ECHO. Considera que la información aquí mostrada corresponde a la versión 2.3.2 donde se puede ver en el listado de cambios como se renombra el soft ack a echo #1292.
Respecto a este tema, también hay una gran discusión en estos enlaces del foro: (1) Auto resend ack y (2) Reliable delivery pero son muy extensos, por eso tras repasarlo todo he decido hacer este resumen.
¿Cómo usar el soft-ack/echo?
Para contemplarlo a nivel de código existen múltiples posibilidades, por eso después de varias pruebas indico aquí ciertas consideraciones para facilitar su uso:
- Al hacer el envío con el ack/echo activo se recibirá una copia del mensaje enviada desde el gateway, pero con un bit de ACK establecido. El envío se hace mediante:
send(msg.set(1),true);
- Hay que tratar adecuadamente las respuesta recibida, El ack/echo se recibe como un mensaje más, pero al ser una copia del original sólo se diferencia en la marca ACK. En el código de comprobación de tipo de mensaje se debe chequear primero que el mensaje es ACK, por ejemplo, mediante:
if(message.isAck() && message.sensor == CHILD_ID_SENSOR)
- El siguiente problema es cuanto tiempo debe pasar para considerar que el ACK/ECHO no se ha recibido, si se espera poco tiempo puede producirse una inundación de la red con reintentos de envío. Lo ideal es usar un temporizador, pero con un contador en el bucle principal en las pruebas que he hecho suele ir bien.
A continuación muestro un código de ejemplo donde se envía un mensaje con
soft-ack/echo activo, donde se establece un contador de espera MotionAck
a 100,
que asegura que no se hará reenvío al menos hasta pasar 5 segundos, ya que que el
bucle tiene una espera de 50ms. En el ejemplo se usa un sensor de movimiento para
hacer pruebas:
// Initialize motion messages
MyMessage msgMot(CHILD_ID_MOT, V_TRIPPED);
int MotionAck = 0;
void loop()
{
static bool last_motion = false;
bool tripped = digitalRead(PIN_MOTION) == HIGH;
if( last_motion != tripped )
{
if ( send(msgMot.set(tripped),true)) { // Solicito echo
MotionAck = 100 ; // Numero de vueltas en el bucle antes de reenviar el mensaje
last_motion = tripped;
}
}
else if(MotionAck > 0)
{
MotionAck--;
if(MotionAck == 0)
{
// Ack/echo no recibido reenvío en mensaje solicitando echo
send(msgMot.set(last_motion),true);
MotionAck = 100;
}
}
// Espero unos milisegundos para no tener que poner MotionAck en uint32
wait(50);
}
void receive(const MyMessage &message)
{
if(message.isAck() && message.sensor == CHILD_ID_MOT)
{
MotionAck = 0 ;
}
}
Integrando ACK/ECHO con HomeAssistant
Otro problema al que también hay que aplicarle una solución parecida es a las pérdidas de mensajes cuando se integra MySensors en HomeAssistant. Para ponerlo en contexto, en la integración de los actuadores (ej. switches) de MySensors con HomeAssistant, los cambios de estado requieren mensajes de confirmación. Se puede comprobar en los ejemplos de la integración de MySensors en HomeAssistant [MySensors Switch], ahí se muestra cómo cuando se recibe un mensaje de cambio de estado, HomeAssistant sólo cambia el estado si recibe un mensaje de vuelta confirmando el nuevo estado.
El problema que me he detectado es cómo a veces desde HomeAssistant activo un interruptor y el nodo remoto enciende la luz remota, pero el mensaje de vuelta se pierde, resultando que, HomeAssistant muestra el interruptor apagado cuando realmente está encendido. Este mismo problema se me agravó con un calefactor de pecera, ya que se activaba, y a veces HomeAssistant no reflejaba el encendido, por lo que no se disparaba el apagado cuando correspondía.
Uniendo el código requerido para HomeAssistant con el código anterior, presento la solución que estoy actualmente usando para los switches, donde es importante tener en cuenta las siguientes consideraciones:
- En la recepción de mensajes es obligatorio distinguir si el mensaje es de tipo ACK/ECHO, ya que es una copia del mensaje original y se podría confundir como una orden/actuación en vez de un ECHO.
- Se debe esperar cierto tiempo (time-out) antes de reenviar de nuevo el mensaje para no inundar la red de mensajes y réplicas.
En el ejemplo propuesto los reintentos son infinitos, aunque habría que planterase poner límites, pero en el tiempo que llevo con esta solución no he detectado problemas, ya que como en el primer ejemplo, tengo calculado el tiempo de espera del blucle para asegurar el reenvio cada 10 segundos. Presento sólo la parte del código que maneja los ACK/ECHO:
// Sw vars
MyMessage msg(CHILD_ID_SW, V_STATUS);
bool SwState = false;
int SwAck = 0;
void loop(){}
...
// Check ACK
if(SwAck > 0)
{
// Waiting ACK
SwAck--;
if(SwAck == 0)
{
// Ack not received, trying again
send(msg.set(SwState),true); // Requested ACK/ECHO
SwAck = 100;
}
}
wait(50);
}
void receive(const MyMessage &message)
{
if(message.isAck() && message.sensor == CHILD_ID_SW)
{
SwAck = 0; // Soft ACK/Echo received disable retries
}
else if (message.type == V_STATUS && message.sensor == CHILD_ID_SW )
{
// HomeAssistant requires the new state confirmed
SwState = message.getBool();
send(msg.set(BSwState),true); // Soft-Ack (echo) requested
SwAck = 100;
}
}