Thursday, February 26, 2015

NRF24L01+/RPi: Sending Messages in 16 Bytes or Less

After playing with some NRF24L01+ message sending from the Raspberry Pi to the Arduino, it seems that the maximum message size it can send in one write transfer is 16 bytes. Just like that, my plan to send JSON strings takes a huge blow unless I implement a loop that will traverse through an oversized char array to get all the message across. But that probably won't be very reliable when you miss a transfer. On reflection, sending strings wasn't a good idea to begin with considering the overhead and inefficiency. I'm going to need to reorganize the message as raw binary with a maximum size of 16 bytes. This sounds like when text messages plans weren't unlimited and you had to word your texts carefully to stay within the limits. Joy.

I'm going to borrow ideas from when I developed for USB Audio on Android. USB uses setup bytes to create "control" transfers. These setup bytes inform the recipient what kind of data we're sending (control, isochronous, bulk) or what direction (host to slave, or vice versa). That's what I'm going to be doing with my NRF24L01+ transfers.

To future-proof this, I will need to implement multiple-transfer data segments if I ever need to send a long string to an Arduino module. The first two bits will determine whether the entire 16-bits is the only single message, final message, or whether there are more coming after this so we can do some simple data integrity checking. "Mid-message" and "final" types would not have other setup bits, it would just be one byte (with the two bits) telling them that they're mid-message or final type and the rest 15 bytes are for data. Like the USB, I would need to know whether this message is meant as a "get" to pull information from the Arduino to RPi (i.e. getting the temperature reading), or whether it's just to set values (i.e. Set thermostat tempertaure, turn lamp on, etc.). That leaves me with 5 bits left in this first byte, these will be used for recipient ID matching so we know who the message is targeted to. This gives me 32 devices (1 RPi + 31 Arduino modules)

1st byte setup:

  • 1-2nd bit: Ordering of transfer (1 = "single", 2 = "initial", 3 = "mid-message", 4 = "final")
  • 3rd bit: 0 = "get", 1 = "set"
  • 4th-8th bit: Recipient device address (32-maximum node points)

Next, I need to be able to send arguments. For example, I have a sensor module with MPU-6050 which has sensors for 6-axes (3 for accelerometer and 3 for gyroscope) plus a temperature. That gives 7 possible values to poll from the RPi. I'm going to believe that 4 bits is enough parameters for each module (16 possible values to get and set). If I need more, that's what the multiple transfer is meant to future-proof.

2nd byte setup:

  • 1-4th bit: Parameters or arguments to deliver (16 possible parameters)
  • 5-6th bit: Predefined Timeout values (4 possible values)
  • 7th bit: Delivered receipt? 1 = "require receipt", 0 = "no receipt"
  • 8th bit: Left blank

One more information is needed, the size of the data. This will use a 16-bit integer so will use up two more bytes. I can go with 8-bits but that means I'm capped at data size of 256 bytes. Not sure how much I will need but better safe than sorry.

3rd-4th byte: Size of data in bytes.

Here is the struct of the message that contains all the setup information on the C++ file for the RPi. For now the MAX_DATA_SIZE is just 1, so I can only send 1 float value at the moment.

// Message type struct
typedef struct Message
{
  char    block[16];              // The actual transfer block
  char    data[MAX_DATA_SIZE];
  int     order;
  int     size;                   // Size of data
  int     param;                  // 0-15 (for preset arguments on receiving end)
  int     type;                   // 0 = get, 1 = set
  int     rcp_addr;               // 5-bit address
  int     timeout;                // 4 possible preset timeout values
  int     reply;
} Message;

So according to what was designed above, the first setup byte would be constructed via:

  // 0x01 = ensure 1 bit
  // 0x03 = ensure 2 bits
  // 0x0F = ensure 4 bits
  // 0x1F = ensure 5 bits
  
  int firstSetupBYte = ((0x03 & m.order) << 6) + ((0x01 & m.type) << 5) + (0x1F & m.rcp_addr);
  int secondSetupByte = ((0x0F & m.param) << 4) + ((0x03 & m.timeout) << 2) + (0x01 & m.reply << 1);

Then I'm going to copy these bytes into the block array of the Message struct and send it out.

  memcpy(&m.block, &firstSetupByte, sizeof(char));
  memcpy(&m.block[1], &secondSetupByte, sizeof(char));
  memcpy(&m.block[2], &m.size, sizeof(short));
  memcpy(&m.block[4], &m.data, sizeof(char) * m.size);
  
  printf("size: %i\n", sizeof(m.data));
  printf("Sending float: %f\n", m.data[0]);
  printf("m.order: %i\n", m.order);
  printf("m.type: %i\n", m.type);
  printf("m.rcp_addr: %i\n", m.rcp_addr);
  printf("m.param: %i\n", m.param);
  printf("m.timeout: %i\n", m.timeout);
  printf("m.reply: %i\n", m.reply);
  
  printf("%s\n", bytesToBits(firstSetupByte));

  radio.write( &m.block, sizeof(char) * 16);

On the Arduino side, I'm going to create the same struct:

// Message type struct
typedef struct Message
{
  char        block[16];
  float       data[MAX_DATA_SIZE];
  short       size;                   // Size of data
  char        order;
  char        param;                  // 0-15 (for preset arguments on receiving end)
  char        type;                   // 0 = get, 1 = set
  char        rcp_addr;               // 5-bit address
  char        timeout;                // 4 possible preset timeout values
  char        reply;
};

After receiving the radio using:

Message m;
radio.read( &m.block, sizeof(char) * 16); 

We need to decode the setup bytes and this is done by the following snippet. This is basically just a reverse of the RPi's code.

      m.order = (0x03 & (m.block[0] >> 6));
      m.type = (0x01 & (m.block[0] >> 5));
      m.rcp_addr = (0x1F & (m.block[0] >> 0));
      m.param = (0x0F & (m.block[1] >> 4));
      m.timeout = (0X03 & (m.block[1] >> 2));
      m.reply = (0x01 & (m.block[1] >> 1));

Here's my full loop code for my Arduino sketch also outputting the entire 16 bytes in binary form.

void loop() {
  
  unsigned long got_time;
  
  if( radio.available()){
    while (radio.available()) {  

      // Get the incoming message
      radio.read( &m.block, sizeof(char) * 16); 
      
      // Copy the first and only block of data (offset by 4 setup bytes)
      memcpy(data.mFloat, m.block + 4, 4);
      
      Serial.print(F("Message.block[16]: \n  "));
      Serial.print(*(byte *)m.block[0], BIN);
      
      // Output all 16 bytes in binary
      for(int i = 1; i < 16; i++){
        Serial.print(" ");
        Serial.print(*(byte *)(m.block[i]), BIN);
      }
      
      Serial.print("\nValue of float: ");
      Serial.println((*(float *)(data.mFloat)));
      
      m.order = (0x03 & (m.block[0] >> 6));
      m.type = (0x01 & (m.block[0] >> 5));
      m.rcp_addr = (0x1F & (m.block[0] >> 0));
      m.param = (0x0F & (m.block[1] >> 4));
      m.timeout = (0X03 & (m.block[1] >> 2));
      m.reply = (0x01 & (m.block[1] >> 1));
      
      Serial.print("m.order: ");
      Serial.println((String)(int)m.order);
      Serial.print("m.type: ");
      Serial.println((String)(int)m.type);
      Serial.print("m.rcp_addr: ");
      Serial.println((String)(int)m.rcp_addr);
      Serial.print("m.param: ");
      Serial.println((String)(int)m.param);
      Serial.print("m.timeout: ");
      Serial.println((String)(int)m.timeout);
      Serial.print("m.reply: ");
      Serial.println((String)(int)m.reply);
      
      
      Serial.print("\n\n");
    }
    
    radio.stopListening();                                        // First, stop listening so we can talk   
    radio.startListening();                                       // Now, resume listening so we catch the next packets.     
   }
} // Loop

Testing this code (sending from the RPi side) sending two different setups:

pi@raspberrypi ~/rf24libs/RF24/examples_RPi $ g++ -Ofast -mfpu=vfp -mfloat-abi=hard -march=armv6zk -mtune=arm1176jzf-s -Wall -I../ -lrf24-bcm send.cpp -o send && sudo ./send pp
size: 4
Sending float: 293.100006
m.order: 2
m.type: 0
m.rcp_addr: 7
m.param: 9
m.timeout: 1
m.reply: 0
00000000 10000111 

pi@raspberrypi ~/rf24libs/RF24/examples_RPi $ g++ -Ofast -mfpu=vfp -mfloat-abi=hard -march=armv6zk -mtune=arm1176jzf-s -Wall -I../ -lrf24-bcm send.cpp -o send && sudo ./send pp
size: 4
Sending float: -99.989998
m.order: 0
m.type: 1
m.rcp_addr: 25
m.param: 2
m.timeout: 0
m.reply: 1
00000000 00111001 

And I get, from my Arduino serial monitor, proper values!

Message.block[16]: 
  10000111 1011 1010011 0 101011 1110011 10101111 0 0 0 0 0 0 0 0 0
Value of float: 293.10
m.order: 2
m.type: 0
m.rcp_addr: 7
m.param: 9
m.timeout: 1
m.reply: 0


Message.block[16]: 
  10111000 10111000 1010011 0 11011011 10010110 11111011 111110 0 0 0 0 0 0 0 0
Value of float: -99.99
m.order: 0
m.type: 1
m.rcp_addr: 25
m.param: 2
m.timeout: 0
m.reply: 1

Pretty exciting stuff when you get things going! In a few days I shall attach my DC relay with a lamp and try to remotely turn the lamp on/off.

No comments :

Post a Comment