I'm trying to have two Teensies talk to each other while doing other stuff.

Here's two functions the master is using to communicate with the slave:
Code:
void communicateWithSlave(int address, int bytesToSend) {
  int index = 0;
  
  Wire.beginTransmission(address);
  Wire.write(bytesToSend);
  Wire.endTransmission();

  Wire.requestFrom(address, bytesToSend);
  while (Wire.available() && (index < bytesToSend)) {
    slaveInBytes[index++] = Wire.read();
  }
}

void querySlaveData(byte module) {
  byte receivedByte = 0;
  int address = (int)module;

  // sending a 1 tells the slave the on next request we'll be quering whether there is any data to receive
  // and that on that next request we'll be asking for one byte only
  communicateWithSlave(address, 1);
  receivedByte = slaveInBytes[0];

  if (receivedByte) {
    // the byte we receive is the number of bytes the slave needs to send to the master
    //requestSlaveData(address, receivedByte);
    communicateWithSlave(address, (int)receivedByte);
    switch (receivedByte) {
      // a 2 returned means we'll be accepting switch (binary) data
      case 2:
        sendI2CtogVal(slaveInBytes[0], slaveInBytes[1], module);
        break;
      // 4 is the tag for int values (check slaveTeensyTemplate.ino)
      case 4:
        sendI2CdcVal(slaveInBytes[0], module, slaveInBytes[1], slaveInBytes[2], slaveInBytes[3]);
        break;
      // 7 is the tag for float values (check slaveTeensyTemplate.ino)
      case 7:
        sendI2CdcVal(slaveInBytes[0], module, slaveInBytes[1], slaveInBytes[2], slaveInBytes[3], slaveInBytes[4], slaveInBytes[5], slaveInBytes[6]);
        break;
      default:
        break;
    }
  }
}
and in the loop function these functions are called like below:
Code:
for (int i = 0; i < numSlaveI2C; i++) {
  querySlaveData(slaveI2CAddresses[i]);
}
where numSlaveI2C is the number of slave Teensies in the system. On the slave side these are the two main functions:
Code:
void receiveEvent(int howMany) {
  int index = 0;
  int sign;
  if (howMany > 1) {
    while (Wire.available()) {
      dataToReceive[index++] = Wire.read();
    }
    // the first byte is the data type (toggle, DC int, or DC float)
    // the rest of the bytes are the data to be stored and processed
    switch (dataToReceive[0]) {
      case 1:
        togRecvVals[dataToReceive[1]] = dataToReceive[2];
        notifySuccessful(); // here the slave is supposed to let the master know it has received all data
        break;
      case 2:
        if (dataToReceive[4]) {
          sign = 1;
        }
        else {
          sign = -1;
        }
        dcIntRecvVals[dataToReceive[1]] = dataToReceive[2] + (dataToReceive[3] * 128);
        dcIntRecvVals[dataToReceive[1]] *= sign;
        debounceEnvIndex(); // here the slave sends back the received byte so the master knows it has gone through and it stops resending it
        break;
      case 3:
        if (dataToReceive[4]) {
          sign = 1;
        }
        else {
          sign = -1;
        }
        float integral = (float)(dataToReceive[2] + (dataToReceive[3] * 128));
        float fractional = (float)((dataToReceive[5] + (dataToReceive[6] * 128)) / (float)dataToReceive[7]);
        dcFloatRecvVals[dataToReceive[1]] = integral + fractional;
        dcFloatRecvVals[dataToReceive[1]] *= (float)sign;
        break;
    }
  }
  else {
    onReceiveNum = Wire.read();
  }
}

// function that executes whenever data is requested by master
// this function is registered as an event, see setup()
void requestEvent() {
  byte sign;
  int dcIntSendVal;
  float dcFloatSendVal;
  switch(onReceiveNum) {
    // when receiving number 1 the slave is being asked whether there is data
    // to send to the master
    case 1:
      dataToSend[0] = haveDataToSend;
      break;
    // when receiving a 2 the slave is being asked to send toggle data
    case 2:
      dataToSend[0] = whichTog;
      dataToSend[1] = togSendVals[whichTog];
      haveDataToSend = 0;
      break;
    // when receiving a 4 the slave is being asked to send dc int data
    case 4:
      dataToSend[0] = whichDC;
      dcIntSendVal = abs(dcIntSendVals[whichDC]);
      if (dcIntSendVals[whichDC] >= 0) {
        sign = 1;
      }
      else {
        sign = 0;
      }
      dataToSend[1] = dcIntSendVal & 0x7f;
      dataToSend[2] = dcIntSendVal >> 7;
      dataToSend[3] = sign;
      haveDataToSend = 0;
      break;
    // when receiving a 7 the slave is being asked to send dc float data
    case 7:
      dataToSend[0] = whichDC;
      dcFloatSendVal = abs(dcFloatSendVals[whichDC]);
      if (dcFloatSendVals[whichDC] >= 0) {
        sign = 1;
      }
      else {
        sign = 0;
      }
      dataToSend[1] = (byte)dcFloatSendVal & 0x7f;
      dataToSend[2] = (byte)dcFloatSendVal >> 7;
      dataToSend[3] = sign;
      // separate the integral and fracional part of the float
      fractional = modf(dcFloatSendVal, &integral);
      // we use hard-coded 2 decimal places so we multiply by 100
      fracAsByte = (byte)(fractional * 100);
      dataToSend[4] = fracAsByte & 0x7f;
      dataToSend[5] = fracAsByte >> 7;
      dataToSend[6] = numDecPlaces;
      haveDataToSend = 0;
      break;
  }
  Wire.write(dataToSend, onReceiveNum);
}
And these are the two debouncing functions along with a function that sets what data must be sent on the next call:
Code:
void setWhichDataToSend (int index, int *pArray, int val) {
  whichDC = index;
  pArray[whichDC] = val;
  haveDataToSend = 4;
}

void debounceEnvIndex() {
  if (dcIntRecvVals[0] != handShakeDebounce) {
    // send the envelope index received back to Pd to I2C receiver 13
    handShakeDebounce = dcIntRecvVals[0];
    setWhichDataToSend(recvDebugIdx, dcIntSendVals, dcIntRecvVals[0]);
  }
}

void notifySuccessful() {
  if (handShakeDebounce != envelopesIndexDebounce) {
    envelopesIndex = handShakeDebounce / 2;
    adsrOrAttributes = handShakeDebounce % 2;
    if (adsrOrAttributes) {
      // update the first rotary encoder
      // the other four use their switch to update attribute values
      encoders[0]->write(envelopes[envelopesIndex][ATTROFFSET]*4);
      writeAttr(envelopes[envelopesIndex], envelopesIndex);
    }
    else {
      for (int i = 0; i < NUMENCS; i++) {
        encoders[i]->write(envelopes[envelopesIndex][i]*4);
      }
      makeEnv(envelopes[envelopesIndex]);
    }
    envelopesIndexDebounce = handShakeDebounce;
    setWhichDataToSend((recvDebugIdx+1), dcIntSendVals, adsrOrAttributes);
  }
}
What I'm trying to do is the following: the master sends one byte, 0x01, to the slave. The slave receiving this byte knows that the master is asking whether the slave has any data to transmit and upon the request sent by the master immediately afterwards, it replies either with 0 for no data, 2 for toggle data (just a 0 or a 1), 4 for int data, and 7 for float (these numbers are actually the number of bytes need to be sent for each data type). The master then tests the received byte with a switch/case structure and decides about the data it will receive. In case the slave has data to send the master send again one byte, this time the byte received by the slave. When the slave receives this byte it knows that upon next request it will transmit the data. The master then requests as many bytes from the slave as the number it received and sent to the slave and the slave sends the necessary bytes.

I know it's a bit too much back and forth, but it seems the only sane way in order for the both the master and the slave to know the number of bytes need to be sent over. So it's actually a transmission, a request, another transmission and another request. It sort of works but some times data sent from the master (whether transmissions or requests) are lost. Both master and slave are doing other things like reading analog pins, rotary encoders, writing to OLED displays etc. Is there a chance that things can go wrong if the two Teensies run at different speeds (one is a 3.6 and the other is an LC)? Anyone knows of a rigid way to have two devices communicate while doing more stuff?