A Proper way to let the emisor know that the receiver has received the packet?

Hello, I'm using the library "LoRaWAN_APP.h" from HelTecAutomation to make a multiple node sensor project with the htcc-ab02a modules. In this project all emisors has to send the data every 12h and then go to deep sleep. The receiver has to read all of them in a short time (30s to 5 min), and then it goes to deep sleep as well.

The problem is that the emisors don't know if the receiver has read the data. It may happen that two or more emisors send at the same time, or they are too far away from the receiver, etc. The most efficient would be that the receiver responds "TX DONE" to each emisor.

There is a "manual" way to solve this: put the emisor in rx mode each time it sends to hear form the receiver the "TX DONE" message; and the receiver in tx mode to send that message; and then repeat the cycle. This is a little messy and it eventually works. But it should exist a better, cleaner, faster way...

So my question is if there is a simpler way to let the emisor know if the receiver read correctly the data from it. Something like:

bool txDone = Radio.send(message); if (txDone == true) Serial.println("The receiver read the message");

Is there such a function in this library or I should try another library? Is there a name for this function/action? It might be standarized. I noticed that LoRaWAN devices has bidirectional communication. See explanation here.

I am using the ClientPubSub library to do the MQTT thing. On the MQTT Broker, a Python program sends "OK" upon receiving a publication to several ESP32's

The ESP32 clients, each time they publish, increment a counter. The counter task resets the ESP32's when a count is reached. The receipt of the "OK" message from the Broker resets the count to 0.

Hopefully this example will show how I am doing the thing:

void IRAM_ATTR mqttCallback(char* topic, byte * payload, unsigned int length)
{
  xSemaphoreTake( sema_mqttOK, portMAX_DELAY );
  mqttOK = 0;
  xSemaphoreGive( sema_mqttOK );
} // void mqttCallback(char* topic, byte* payload, unsigned int length)

void fmqttWatchDog( void * paramater )
{
  int maxNonMQTTresponse = 5;
  for (;;)
  {
    vTaskDelay( 1000 );
    if ( mqttOK >= maxNonMQTTresponse )
    {
      ESP.restart();
    }
  }
  vTaskDelete( NULL );
} //void fmqttWatchDog( void * paramater )

void fDoMoistureDetector( void * parameter )
{
  //wait for a mqtt connection
  while ( !MQTTclient.connected() )
  {
    vTaskDelay( 250 );
  }
  /*
  */
  int64_t  EndTime = esp_timer_get_time();
  int      TimeToWait = 5000000;
  uint64_t TimePast = esp_timer_get_time(); // used by the Kalman filter
  float    WetValue = 1.35f; // point to deenergize relay for water pump
  float    DryValue = 2.2f; // point to energize relay
  float    ADbits = 4095.0f;
  float    uPvolts = 3.3f;
  //  float    adcValue_a = 0.0f;
  float    adcValue_b = 0.0f; //Jeanne's potted plant in yellow pot
  float    Range = DryValue - WetValue;
  float    RemainingMoisture = 0.0f;
  //SimpleKalmanFilter KF_ADC_a( 1.0f, 1.0f, .01f );
  SimpleKalmanFilter KF_ADC_b( 1.0f, 1.0f, .01f );
  for (;;)
  {
    //    adcValue_a = float( adc1_get_raw(ADC1_CHANNEL_0) ); //take a raw ADC reading
    //    adcValue_a = ( adcValue_a * uPvolts ) / ADbits; //calculate voltage
    //    KF_ADC_a.setProcessNoise( (esp_timer_get_time() - TimePast) / 1000000.0f );
    //    adcValue_a = KF_ADC_a.updateEstimate( adcValue_a ); // apply simple Kalman filter
    //
    adcValue_b = float( adc1_get_raw(ADC1_CHANNEL_3) ); //take a raw ADC reading
    adcValue_b = ( adcValue_b * uPvolts ) / ADbits; //calculate voltage
    KF_ADC_b.setProcessNoise( (esp_timer_get_time() - TimePast) / 1000000.0f );
    adcValue_b = KF_ADC_b.updateEstimate( adcValue_b ); // apply simple Kalman filter
    //
    RemainingMoisture = 100.0f * (Range / adcValue_b); //calculate remaining moisture
    //energis or denergize water pump
    if ( RemainingMoisture <= 20.0f )
    {
      gpio_set_level( GPIO_NUM_5, LOW); //energize
    } else {
      //keep pump on till value falls below
      if ( RemainingMoisture >= 85.0f )
      {
        gpio_set_level( GPIO_NUM_5, HIGH); // deenergize
      }
    }
    log_i( "adcValue_b = %f remaining moisture %f", adcValue_b, RemainingMoisture );
    // publish to MQTT every 5000000uS
    if ( (esp_timer_get_time() - EndTime) >= TimeToWait )
    {
      //then publish
      xSemaphoreTake( sema_MQTT_KeepAlive, portMAX_DELAY ); // whiles MQTTlient.loop() is running no other mqtt operations should be in process
      MQTTclient.publish( topicRemainingMoisture_0, String(RemainingMoisture).c_str() );
      xSemaphoreGive( sema_MQTT_KeepAlive );
      xSemaphoreTake( sema_mqttOK, portMAX_DELAY ); // update mqttOK
      mqttOK++;
      xSemaphoreGive( sema_mqttOK );
      EndTime = esp_timer_get_time(); // get next publish time
    }
    TimePast = esp_timer_get_time();
    vTaskDelay( 100 ); //good refresh rate
    //log_i( " high watermark %d",  uxTaskGetStackHighWaterMark( NULL ) );
  }
  vTaskDelete( NULL );
}// end fDoMoistureDetector()

The task void IRAM_ATTR mqttCallback simply, sets the counter back to 0

The task void fDoMoistureDetector( void * parameter ) increments the counter with each sending.

The task void fmqttWatchDog( void * paramater ) checks the count and if the count is greater than or equal to a count then rest the ESP.