Feather M0 with LMIC takes more than 2 minutes to transmit

Hi,
I’m having trouble with understanding an issue with an end-node prototype I’m working on.
This end-node is constituted by a Feather M0 LoRa for the EU region and three sensors (DHT22, BMP388 and a contact sensor) and it sends the data collected by the sensors encoded in a payload of 10 bytes. I’m using this LMiC library to handle the LoRa communication and the Arduino LowPower and the RTCZero libraries for the deep sleep implementation, plus all the standard libraries needed by the sensors.
I have 4 Feathers with which I’m testing the code, one with all the above mentioned sensors and the other 3 with all the sensors minus the BMP388. The join procedure chosen is OTAA (widely tested, it works).

The issue I’m experiencing with all four devices on which i loaded the sketch is that the end-nodes transmission period is 1 hour, 2 minutes and 20 seconds (±10 seconds) but the sleep time I’ve specified in the code is an hour (therefore the end-node stays on for circa 2’ and 20’’ for each transmission).
I suspect that this problem is somehow induced by the deep sleep cycle, since I’ve tried the same sketch replacing that function with a simple delay of the same length and the time needed to transmit dropped to more “acceptable” numbers: I kept track of it using the elapsedMillis library and the most common values were 5 or 17 seconds, rarely they went up to 30-45 seconds (including the code execution).

This is the content of the lmic_project_config.h file:
#define CFG_eu868 1
#define CFG_sx1276_radio 1
#define LMIC_USE_INTERRUPTS
#define DISABLE_PING
#define DISABLE_BEACONS
#define LMIC_DEBUG_LEVEL 1

Here are the relevant parts of the code I’m using (it’s more than 500 lines long so I thought it would be better to include only these):

const lmic_pinmap lmic_pins = {
  .nss = 8,
  .rxtx = LMIC_UNUSED_PIN,
  //.rst = 4,
  .rst = LMIC_UNUSED_PIN,
  .dio = {3, 6, LMIC_UNUSED_PIN},
  .rxtx_rx_active = 0,
  .rssi_cal = 8,              // LBT cal for the Adafruit Feather M0 LoRa, in dB
  .spi_freq = 8000000,
};

void setup() {
  pinMode(DHT_PIN,INPUT);
  pinMode(CONTACT_SENSOR_PIN,INPUT_PULLUP);
  pinMode(BATTERY_PIN, INPUT);

  delay(3000);

  rtc_sleep.begin();
  rtc_sleep.setEpoch(0);

  // The code from here to detachInterrupt() has been adapted from a post I've found online
  // and it is used to make the interrupts work on the edges (RISING, FALLING and CHANGE
  // I don't really understand it and I'd be curious to understand its impact on the power draw during
  // the deep sleep
  attachInterrupt(digitalPinToInterrupt(CONTACT_SENSOR_PIN), interrupt_RISING, RISING);

  // Set the XOSC32K to run in standby
  SYSCTRL->XOSC32K.bit.RUNSTDBY = 1;

  // Configure EIC to use GCLK1 which uses XOSC32K
  // This has to be done after the first call to attachInterrupt()
  GCLK->CLKCTRL.reg = GCLK_CLKCTRL_ID(GCM_EIC) |
                     GCLK_CLKCTRL_GEN_GCLK1 |
                     GCLK_CLKCTRL_CLKEN;

  detachInterrupt(digitalPinToInterrupt(CONTACT_SENSOR_PIN));

  dht.begin();

  bmp.begin();

  // LMIC init
  os_init();
  // Reset the MAC state. Session and pending data transfers will be discarded.
  LMIC_reset();

  // Disable link-check mode and ADR, because ADR tends to complicate testing.
  LMIC_setLinkCheckMode(0);
  // Set the data rate to Spreading Factor 7.  This is the fastest supported rate for 125 kHz channels, and it
  // minimizes air time and battery power. Set the transmission power to 14 dBi (25 mW).
  LMIC_setDrTxpow(DR_SF7,14);
  // in the US, with TTN, it saves join time if we start on subband 1 (channels 8-15). This will
  // get overridden after the join by parameters from the network. If working with other
  // networks or in other regions, this will need to be changed.
  // I'm in the EU region, do I need to call this function with other parameters..?
  //LMIC_selectSubBand(1);  
  LMIC_setClockError(MAX_CLOCK_ERROR * 10 / 100);
  // Start job (sending automatically starts OTAA too)
  do_send(&sendjob);
}

void loop() {
  os_runloop_once();
}

// I included only the TX_COMPLETE event because it's the only one in which I actual do something
void onEvent (ev_t ev) {
  Serial.print(os_getTime());
  Serial.print(": ");
  switch(ev) {              
          
      case EV_TXCOMPLETE:
      
          // Enable the interrupt before the deep sleep
          attachInterrupt(digitalPinToInterrupt(CONTACT_SENSOR_PIN), interrupt_RISING, RISING);
          // Call deep sleep function
          goToSleep(TX_interval);
          // Disable the interrupt after the deep sleep
          detachInterrupt(digitalPinToInterrupt(CONTACT_SENSOR_PIN));
          
          do_send(&sendjob);
          break;
    }
  }

void goToSleep (int sleep_time) {
  rtc_sleep.setEpoch(0);            // Probably not needed..
  
  long unsigned int curr_time = rtc_sleep.getEpoch();               // Start deep sleep epoch
  long unsigned int wake_up_time = curr_time + sleep_time;        // End deep sleep epoch

  // Auxiliary variable needed in case the deepSleep function is stopped by an interrupt
  long unsigned int sleep_time_left = sleep_time;

  while (curr_time < wake_up_time) {
    sleep_time_left = wake_up_time - curr_time;      
    
    if (sleep_time_left > 0) {
      LowPower.deepSleep(sleep_time_left * 1000);   // * 1000 poiché il tempo qua è in ms
    } else {
      wake_up_time = 0;
    }
    
    curr_time = rtc_sleep.getEpoch();
  }
}

void do_send(osjob_t* j){
    // Check if there is not a current TX/RX job running
    if (LMIC.opmode & OP_TXRXPEND) {
        Serial.println(F("OP_TXRXPEND, not sending"));
    } else {
        DHT_reading();
        door_state = digitalRead(CONTACT_SENSOR_PIN);
        battery_reading();
        BMP_reading();

        preparePayload();
        
        door_openings = 0;

        /* prepare upstream data transmission at the next possible time.
         * transmit on port 1 (the first parameter); you can use any value from 1 to 223 (others are reserved).
         * don't request an ack (the last parameter, if not zero, requests an ack from the network).
         * Remember, acks consume a lot of network resources; don't ask for an ack unless you really need it. */
        tx_error_code = LMIC_setTxData2(1, payload, sizeof(payload), 0);
        handle_tx_errors();   // Just built the skeleton of the function, at the moment it's almost empty
    }
}

I’ve not included the code for the sensors readings, the payload preparation and other stuff but I can guarantee that those parts have been tested and they work as expected (other than the issue subject of the post everything works perfectly, the payload arrives as expected, etc.).
When I tried this sketch with the delay() function in place of the deep sleep cycle, I monitored the time needed to execute the code between just before the do_send execution in TX_COMPLETE and the end of its execution (which corresponds to the sensor readings and the payload preparation - everything that the sketch does between two transmissions) and it amounted to 1.3 seconds.

You may want to ensure everything is interrupt safe, in general you may be better served by setting flags in onEvent and handling them in your main loop.

For testing, you may want to toggle an IO pin high and low while a transfer is occurring to narrow down where the program is spending time. An NRF power profiler (or other power consumption monitor) can also help, it makes it easy to see when the radio is on and sending or listening.