How to set up a command-based communication using I2C

We are almost there.

We don't use readable ASCII for the I2C bus.
A fixed size of binary data is achieved this way:
[...]
or with a struct:

I like the idea of having a struct of fixed size, I somehow forgot that there are structs in C.

if( howMany == 18)       // expecting 18 bytes, ignore everything else

That's a very good check. I will use that too.

Calling Wire.write() outside requestEvent() is undefined behaviour. Please follow the examples.

I have a hard time understanding how an API call can be undefined. A callback/handler is a function call based on an interrupt; a function call is a jump instruction; why shouldn't it be possible to execute certain instructions outside of that?

Why would you use a while-statement ? How many times do you want to read the package of data ? [...] That's how the Wire library examples receive I2C input. [...] I know ! and it is really bad.

I think we all agree that the provided examples and built-in libraries might be not optimal. I mean, if you follow the API calls, you will eventually find out that any read operation on the I2C bus is blocking..

Anyway, back to the task. Since the nRF is a 32-Bit CPU, I defined my struct to fit within 32 bits, otherwise another 32 bits would be allocated.

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

Adjusted master code:

#include "Wire.h"

#define M0_I2C_SLAVE_ADDRESS 13
#define SERIAL_BUFFER_LEN 64
#define COMMAND_LEN 4

enum {
    CMD_TUNE = 169,
    CMD_SERVICE = 172,
    CMD_INFO = 142,
    CMD_STATUS = 213
};

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

void sendCommand(Command commandStruct, uint8_t responseSize){
  Wire.beginTransmission(M0_I2C_SLAVE_ADDRESS);
  Wire.write((byte *)&commandStruct, COMMAND_LEN);
  Wire.endTransmission ();
  if (Wire.requestFrom(M0_I2C_SLAVE_ADDRESS, responseSize) == 0){
    Serial.println("Error");
  }else{
    char buffer[3];
    buffer[0] = Wire.read();
    buffer[1] = Wire.read();
    buffer[2] = Wire.read();
    Serial.print("Response from M0 board: ");
    Serial.print(buffer[0]);
    Serial.print(buffer[1]);
    Serial.print(buffer[2]);
    Serial.print('\n');
  }
}
  
void setup(){
  Wire.begin();
  Serial.begin(115200);
}

void loop(){
  if (Serial.available()){
      uint8_t txData[SERIAL_BUFFER_LEN];
      size_t bytesRead = Serial.readBytes(txData, SERIAL_BUFFER_LEN);
      if(txData[0]=='1'){
        Command commandTune = {
          .cmd = CMD_TUNE,
          .payload = 107500
        };
        sendCommand(commandTune,3);
      }else if(txData[0]=='2'){
        Command commandService = {
          .cmd = CMD_SERVICE,
          .payload = 14
        };
        sendCommand(commandService,3);
      }else if(txData[0]=='3'){
        Command commandInfo = {
          .cmd = CMD_INFO,
          .payload = 0
        };
        sendCommand(commandInfo,3);
      }else{
        // do nothing
      }
  }
}

Slave code:

#include "Wire.h"

#define M0_I2C_SLAVE_ADDRESS 13
#define COMMAND_LEN 4

enum {
    CMD_TUNE = 169,
    CMD_SERVICE = 172,
    CMD_INFO = 142,
    CMD_STATUS = 213
};

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

volatile uint8_t printRequest = 0;
volatile Command bufferedCommand = {.cmd = 0, .payload = 0};

void setup() {
  Wire.begin(M0_I2C_SLAVE_ADDRESS);
  Wire.onReceive(receiveEvent);
  Wire.onRequest(requestEvent);
  SerialUSB.begin(115200); 
}

void receiveEvent(int nBytes){
  //SerialUSB.println("Callback receiveEvent()!");  // still gets called twice with each transmission
  if(nBytes != COMMAND_LEN){
    SerialUSB.print("Length error: ");
    SerialUSB.println(nBytes,DEC);
  }else{
    byte cmdBuffer[COMMAND_LEN];
    for (uint8_t i=0; i<COMMAND_LEN; i++){
      cmdBuffer[i] = Wire.read();
    }
    bufferedCommand.cmd = cmdBuffer[0];
    bufferedCommand.payload = (uint32_t) cmdBuffer[3] << 16;
    bufferedCommand.payload |= (uint32_t) cmdBuffer[2] << 8;
    bufferedCommand.payload |= (uint32_t) cmdBuffer[1];
    printRequest = 1;
  }
}

void requestEvent (){
  if(bufferedCommand.cmd==CMD_TUNE){
      Wire.write('y');
      Wire.write('o');
      Wire.write('!');
  }else if(bufferedCommand.cmd==CMD_SERVICE){
      Wire.write('f');
      Wire.write('o');
      Wire.write('o');
  }else if(bufferedCommand.cmd==CMD_INFO){
      Wire.write('4');
      Wire.write('5');
      Wire.write('6');
  }else if(bufferedCommand.cmd==CMD_STATUS){
      Wire.write('x');
      Wire.write('y');
      Wire.write('z');
  }else{
      SerialUSB.println("No valid command!");
  }
}

void loop() {
  if(printRequest){
    SerialUSB.print("command: ");
    SerialUSB.print(bufferedCommand.cmd, DEC);
    SerialUSB.print('\n');
    SerialUSB.print("Payload: ");
    SerialUSB.println(bufferedCommand.payload, DEC);
    printRequest = 0;
  }
}

This works quite well expect for the fact that the callback receiveEvent() still gets called twice at each transmission for some reason. This makes the behaviour sometimes a bit buggy. In one off the calls, nBytes is equal to COMMAND_LEN and everything works as expected. In the other call, nBytes is equal to zero and the error message pops up.