When the answer is already waiting, then you can do a request directly after sending the command.
The Master determines which Slave is selected, either for sending a command or requesting data. You can mix everything and even get data from a sensor in between and it will be allright.
The code in the receiveEvent() is okay. I think the Arduino rushes fast enough through that. But you have to remove the Serial.println() from the processMode() function.
The I2C bus is a "bus", however it is not designed to go through a cable or long wires. A rule of thumb is a maximum of 50 cm total length. The crosstalk between SDA and SCL is the worst, so don't put those next to each other in a flat ribbon cable.
Which Arduino boards do you use as a Slave and if they are 5V boards then what have you done to solve the voltage mismatch for SDA and SCL. What are your pullup resistors and where are they ?
You have added a checkum, but there is something that can go wrong. When the command checksum was wrong, then the Slave did not accept it. The Master requests data anyway, without knowing that the command was not accepted.
It is possible to detect that if you add a identifier to the command (for example a value that increments for every command), then you can check in the requested data if it has the same identifier.
You don't check the error returned by Wire.endTransmission() or if Wire.requestFrom() failed. Your ::digitalRead() and ::analogRead() have no way to tell if there was an error, because the return value is already used for data. What if a Slave is not connected to the I2C bus at all ?
This is an alternative, using 'howMany':
void receiveEvent(int howMany)
{
if( howMany > 0 && howMany <= 6) { // also check if not zero (extra safety check)
uint8_t buf[6];
Wire.readBytes( buf, howMany);
if( validateChecksum((uint8_t*) buf, (uint8_t) howMany)) {
switch( buf[0]) {
case PIN_WRITE:
processWrite((uint8_t*) buf, (uint8_t) howMany);
break;
case PIN_READ:
processRead((uint8_t*) buf, (uint8_t) howMany);
break;
case PIN_MODE:
processMode((uint8_t*) buf, (uint8_t) howMany);
break;
}
}
}
}
The Wire.readBytes() is from the higher level Stream class. You can also use a for-loop up to 'howMany' and do Wire.read().