How to set up a command-based communication using I2C

Yes, I called that a "misfire". That is a bug. For safety you have to filter those out anyway.

You are very polite.

Please don't try to be smart and think ahead of a problem that does not exist. Always follow the KISS principle.

You do this:

typedef struct Command{
  uint8_t cmd : 8;
  uint32_t payload : 24; 
}Command;

Every Arduino board puts the struct elements at byte order to be compatible with all the other Arduino boards. Please don't use bit definitions, use normal uint8_t (or byte) or an array of bytes or other variables.
By the way, 24 bits is only 3 bytes. Your struct uses 5 bytes and only 4 are used. That is what happens if you forget about the KISS principle :wink:

There is a timing thing.
When a fast Master does a I2C write and a I2C read session, then a slower Slave might not be ready. Especially the slower boards such as a Arduino Uno needs "time to breathe".

Don't be surprised if things work better with a delay:

  Wire.beginTransmission(M0_I2C_SLAVE_ADDRESS);
  Wire.write((byte *)&commandStruct, COMMAND_LEN);
  int error = Wire.endTransmission ();

  delay(1);                            // give Slave "time to breathe"

  if (Wire.requestFrom(M0_I2C_SLAVE_ADDRESS, responseSize) == responseSize)  // all okay ?
  {
    Wire.readBytes( buffer, responseSize);       // <-- this is weird, but allowed
  }

Here is a puzzle for you:
Look at the loop() of the Slave. Suppose that some time ago the printRequest was set to 1 and the loop() detects that. Then suddenly new data arrives via the I2C bus while the loop() is still busy between the if(printrequest){ and the printRequest=0.
Then you have half the old data and half the new data.
Solution: make some kind of handshake; or make of copy of the struct and clear the flag with the interrupts turned off; or make an error counter in the onReceive handler.