Struggling to understand nRF24L01 radio "pipes".

Hi all,

I've been trying to follow along in the nice tutorial thread by Robin2,
but there are a few things I keep tripping up on.

For brevity, heres the code im using,
with comments in them of me trying to make sense of it;

TRANSMITTER SIDE :

#include <SPI.h> // Lets arduino talk to nRF24L01 radio module.
#include <nRF24L01.h> // Not sure, subsidiary to RF24.h ?
#include <RF24.h> // Translates functions to actual communication.

const byte server[5] = {'R', 'x', 'A', 'A', 'A'}; // address of server.

RF24 radio(9, 10); // Defines radio com lines.

char dataToSend[10] = "Message 0";  // Static part of message.
char txNum = '0';                   // Changing part of message.

// Timer variables.
unsigned long currentMillis;
unsigned long prevMillis;
unsigned long txIntervalMillis = 1000; // send once per second

void setup() {

  Serial.begin(9600); // Open serial feed.
  Serial.println("SimpleTx Starting"); // Announce start.

  radio.begin();                      // Starts radio.
  radio.setDataRate( RF24_250KBPS );  // Transmission rate.
  radio.setRetries(3, 5);             // Delay, Count.
  radio.openWritingPipe(server);        // Adress of server.
}

void loop() {
  // Loop just checks if millis increased by 1000 or more,
  // triggering a send() function if it has.
  currentMillis = millis();
  if (currentMillis - prevMillis >= txIntervalMillis) {
    send();
    prevMillis = millis();
  }
}

void send() {

  // rslt set to TRUE if transmission acknowledged,
  // blocks until out of tries, or success.
  bool rslt;
  rslt = radio.write( &dataToSend, sizeof(dataToSend) );

  // Announces what has been sent.
  Serial.print("Data Sent ");
  Serial.print(dataToSend);

  if (rslt) {
    // Announces that it has succeeded.
    Serial.println("  Acknowledge received");
    updateMessage(); // Cycles dynamic part of message by one.
  }
  else {
    // Announces that it has failed.
    Serial.println("  Tx failed");
  }
}

void updateMessage() {
  txNum += 1;
  if (txNum > '9') {
    txNum = '0';
  }
  dataToSend[8] = txNum;
}

RECEIVER SIDE:

#include <SPI.h> // Lets arduino talk to nRF24L01 radio module.
#include <nRF24L01.h> // Not sure, subsidiary to RF24.h ?
#include <RF24.h> // Translates functions to actual communication.

const byte server[5] = {'R', 'x', 'A', 'A', 'A'}; // This servers address.

RF24 radio(9, 10); // Defines radio com lines.

char dataReceived[10]; // this must match dataToSend in the TX
bool newData = false;

void setup() {

  Serial.begin(9600); // Open serial feed.
  Serial.println("SimpleRx Starting"); // Announce start.

  radio.begin();                      // Starts radio.
  radio.setDataRate( RF24_250KBPS );  // Transmission rate.
  radio.openReadingPipe(1, server);   // This confuses me.
  radio.startListening();             // Start listening on pipe 1.
}

void loop() {
  // If radio has new data; fetch and raise flag.
  if ( radio.available() ) {
    radio.read( &dataReceived, sizeof(dataReceived) );
    newData = true;
  }
  // If flag has been raised; present data, lower flag.
  if (newData == true) {
    Serial.print("Data received ");
    Serial.println(dataReceived);
    newData = false;
  }
}

What confuses me is the receiver specifying a pipe (openReadingPipe).
The server has a 5 byte address, but then there are also 6 pipes?
The transmitting side does not specify a pipe, so how does this work?
Can I have multiple transmitters all calling the same address?
And if so, how can the receiver tell them apart?
Im sure its in a manual somewhere, but my googlefoo is failing me.

Can somebody throw me some hints? :stuck_out_tongue:

Thanks,

  • Soko

May be more food for thoughts in this reading here

remember if you define an address with an array such as const byte server[5] = {'R', 'x', 'A', 'A', 'A'}; then 'R' comes first in memory address then 'x" etc. But as Arduino uses little endian, if you define the address with a long number such as 0x000000B[sub]1[/sub]B[sub]2[/sub]B[sub]3[/sub]B[sub]4[/sub]B[sub]5[/sub][color=blue]LL[/color] then byte B5 will be coming first in memory, followed by B4, then B3, ...

I've looked at the suggested website, but I can't figure out how many-to-one would work.
Something about masking or bitshifting, but I fail to understand what that means.

I understood that the address involves 5 bytes, but I don't know what your explanation means.
Giving it a raw 5 byte number makes it go.. funny?

Im kinda gathering that the receiving end will have to issue 5 addresses,
and open them for reading on pipes 1 through 5 respectively to be able to distinguish them?
So instead of

const byte server[5] = {'R', 'x', 'A', 'A', 'A'};

void setup() {
  radio.begin();
  radio.setDataRate( RF24_250KBPS );
  radio.openReadingPipe(1, server);
  radio.startListening();
}

I would have to do

const byte node1[5] = {'R', 'x', 'A', 'A', 'A'};
const byte node2[5] = {'R', 'x', 'A', 'A', 'B'};
const byte node3[5] = {'R', 'x', 'A', 'A', 'C'};
const byte node4[5] = {'R', 'x', 'A', 'A', 'D'};
const byte node5[5] = {'R', 'x', 'A', 'A', 'E'};

void setup() {
  radio.begin();
  radio.setDataRate( RF24_250KBPS );
  radio.openReadingPipe(1, node1);
  radio.openReadingPipe(2, node2);
  radio.openReadingPipe(3, node3);
  radio.openReadingPipe(4, node4);
  radio.openReadingPipe(5, node5);
  radio.startListening();
}

Is this correct?
I'm not even sure how to see what transmitting source 'radio.available' would originate from.

I wish I could find an example code of a master hub listening to multiple slave nodes.
Perhaps I can make more sense of it when I see some functional code. :roll_eyes:

Each of your transmitters would have to identify itself with the transmitted message. A message could be constructed like this (examplatory code):

struct RF_MESSAGE {
  uint8_t device_id; //Identifier of the device, set by the transmitter
  uint32_t message_id; //Unique identifier of the message
  uint32_t data; //The data being transmitted
};

//Transmitter 1
static uint32_t msg_counter = 0;
RF_MESSAGE msg;
msg.device_id = 1;
msg.message_id = msg_counter++;
msg.data = 1234;
radio.write(&msg, sizeof(msg));

//Transmitter 2
static uint32_t msg_counter = 0;
RF_MESSAGE msg;
msg.device_id = 2;
msg.message_id = msg_counter++;
msg.data = 5678;
radio.write(&msg, sizeof(msg));

//Receiver
if (radio.available()) {
  RF_MESSAGE msg;
  radio.read(&msg, sizeof(msg));
  switch (msg.device_id) {
    case(1):
      handleTransmitter1(msg);
      break;
    case(2):
      handleTransmitter2(msg);
      break;
  }
}

This approach would also work well with one transmitter and multiple receivers (without ack, though).

Sokonomi:
What confuses me is the receiver specifying a pipe (openReadingPipe).
The server has a 5 byte address, but then there are also 6 pipes?

There are probably very few situations in which it is necessary to use more than one pipe.

Think of the pipes as a bunch of post-boxes in an apartment building. Each post box has the apartment number and when a letter is popped through the letter box someone looks at it, checks the apartment number and puts it into to the post-box for that apartment. When the occupier comes home s/he checks his/her post-box and collects the letter. The nRF24 channel serves the same purpose as the apartment building address - everything on that channel is received. The address fulfils the same role as the apartment number - allowing messages to be segregated into different groups (pipes).

From this you should understand why it is not necessary for the transmitter to specify a pipe.

You can have as many transmitters as you wish all sending to the same address. (Just like having several postmen calling). However if two (or more) transmitters send on the same channel at the same time all the messages will be garbled.

A simple way to know where a message came from is to include a byte in the message that identifies the sender.

The Nordic nRF24L01+ datasheet has all the technical data but IIRC it does not describe appropriate logical strategies for using the facilities.

...R

1 Like

This code

const byte node1[5] = {'R', 'x', 'A', 'A', 'A'};
const byte node2[5] = {'R', 'x', 'A', 'A', 'B'};
const byte node3[5] = {'R', 'x', 'A', 'A', 'C'};
const byte node4[5] = {'R', 'x', 'A', 'A', 'D'};
const byte node5[5] = {'R', 'x', 'A', 'A', 'E'};

will not work, you are changing the wrong byte.

nrf24 radio modules typically use a 40-bit address format, requiring 5-bytes of storage space per address

in the RF24 library, you originally would open a pipe for writing via a uint64_t (unsigned long long) so that you get 8 bytes but would only use the 5 least significant bytes). that was the method

void RF24::openWritingPipe(uint64_t address)

because Arduino is little endian, when using 8 bytes numbers to represent an address, you would get the LSB as the first byte in memory and that was the one used to differentiate pipes.

In the recent version of the library, a byte array is used and so the layout in the array is the same as in memory and thus the first byte of the array is the one that needs to be different.

You can set the length of the addresses you use (and the old addressing format has been retained for compatibility).

This leads to English pipe names such as

uint8_t addresses[][6] = {"[color=red]1[/color]Node","[color=red]2[/color]Node"};
radio.openWritingPipe(addresses[0]);

or purely numeric ones

uint8_t address[] = { [color=red]0xCC[/color],0xCE,0xCC,0xCE,0xCC };
radio.openWritingPipe(address);

changing for another address would be done by changing the byte at index 0.

address[ [color=red]0[/color]] = [color=red]0x33[/color];
radio.openReadingPipe(1,address);

(I colored in Red the byte that will need to be changed for differentiating pipes).

As the RF24 lib doc states:

Up to 6 pipes can be open for reading at once.

Pipes 0 and 1 will store a full 5-byte address.

Pipes 2-5 will technically only store a single byte, borrowing up to 4 additional bytes from pipe #1 per the assigned address width.

Pipes 1-5 should share the first 32 bits (4 bytes). [note: this is where the doc is confusing, "first" is unclear]

Pipe 0 is also used by the writing pipe. So if you open pipe 0 for reading, and then startListening(), it will overwrite the writing pipe.

Ha, if it isn't Robin2 him/herself! Thanks for chiming in, your postbox analogy is a good one, I understand now. Using different pipes and addresses is only useful in specific situations. Not something I should worry about if I just want a listener to hear a motion sensor go "ping", I assume. Pipes are kind of like a basic version of topics in MQTT I guess. :stuck_out_tongue: You can "subscribe" to them too, more or less. Maybe it ll come in handy later, if I want to slip in some temp/humidity data that have to be handled differently from intrusion triggers?

Crossfiring signals shouldn't be too much of an issue, unless people start jumping around in different rooms at exactly the same time (apparently sending a packet is a matter of milliseconds). But I think you can mediate that by letting each node have a different retry delay, so that IF it happens to overlap, the retry will occur at different times. Letting device_id factor in on delay time will automatically make it differ.

@J-M-L Your initial response with the link makes a lot more sense now that you've explained it. That page showed changing the first byte in the code, but the images all showed the end byte changing. The little endian part didn't click with me initially. :wink: Thanks!

So then, based on all that info ive come up with this one;

TRANSMITTER NODE 1:

#include <SPI.h>
#include <nRF24L01.h>
#include <RF24.h>

const byte server_id[5] = {'R', 'x', 'A', 'A', 'A'};
const int node_id = 1;

RF24 radio(9, 10);

struct RF_MESSAGE {
  uint8_t device_id;
  uint32_t message_id;
  uint32_t data;
};

unsigned long currentMillis;
unsigned long prevMillis;
unsigned long txIntervalMillis = 1000;

static uint32_t msg_counter = 0;
    RF_MESSAGE msg;
    msg.device_id = node_id;
    msg.message_id = msg_counter++;
    msg.data = 1234;

void setup() {
  Serial.begin(9600);
  Serial.print("Node");
  Serial.print(node_id);
  Serial.println(" starting..");
  radio.begin();
  radio.setDataRate( RF24_250KBPS );
  radio.setRetries(node_id, 5);
  radio.openWritingPipe(server_id);
}

void loop() {
  currentMillis = millis();
  if (currentMillis - prevMillis >= txIntervalMillis) {
    bool rslt;
    rslt = radio.write(&msg, sizeof(msg));
    Serial.print("Data Sent [");
    Serial.print(msg);
    if (rslt) Serial.println("] Acknowledge received");
    else Serial.println("] Tx failed");
    prevMillis = millis();
  }
}

SERVER :

#include <SPI.h>
#include <nRF24L01.h>
#include <RF24.h>

const byte server_id[5] = {'R', 'x', 'A', 'A', 'A'};

RF24 radio(9, 10);

void setup() {

  Serial.begin(9600); // Open serial feed.
  Serial.println("Server Starting");

  radio.begin();
  radio.setDataRate( RF24_250KBPS );
  radio.openReadingPipe(1, server_id);
  radio.startListening();
}

void loop() {
  if (radio.available()) {
    RF_MESSAGE msg;
    radio.read(&msg, sizeof(msg));
    switch (msg.device_id) {
      case (1):
        // Node 1 sent data, handle it here.
        break;
      case (2):
        // Node 2 sent data, handle it here.
        break;
    }
  }
}

Node1 will send a data packet containing [device_id,message_id,data] once every second, addressed to server 5278414141.

Am I getting close now? :slight_smile:

You are :slight_smile:

if you use different architectures you might want to pack your structure and possibly put the single byte at the end since some compiler might align fields on 32 bits.

struct __attribute__((packed)) RF_MESSAGE {
  uint32_t message_id;
  uint32_t data;
  uint8_t device_id;
};

Not sure what you expect from the msg_counter++ in this initialization outside any function code

    RF_MESSAGE msg;
    msg.device_id = node_id;
    msg.message_id = msg_counter++;
    msg.data = 1234;

Note that your device_id is one byte and you define node_id as an intconst int node_id = 1;you probably want a uint8_t too.

you don't really need to have different retries delays for the different nodes, the Enhanced ShockBurst mode will handle that for you.

Thanks for helping me, I've learned a ton of new tricks already!

I'm not sure what you mean with different architecture and packing it, but from what I can gather it strips padding and makes the payload smaller? That's useful, so I will implement it. :slight_smile: I did see some people postfixing it to the struct instead though, is that important, or is that just personal preference? It would look like this;

struct RF_MESSAGE {
  uint32_t message_id;
  uint32_t data;
  uint8_t device_id;
} __attribute__((packed));

As for the msg_counter++; part, I might have jumped the gun just pasting it. :sweat_smile: It wouldn't do anything outside function. I'll have another go at that.

Setting node_id to a bytesize integer is probably a good idea if its gonna feed into struct RF_MESSAGE, so I'll fix that too.

Attempt number three;

#include <SPI.h>
#include <nRF24L01.h>
#include <RF24.h>

RF24 radio(9, 10);

const byte server_id[5] = {'R', 'x', 'A', 'A', 'A'};

struct __attribute__((packed)) RF_MESSAGE {
  uint32_t message_id;
  uint32_t data = 1;
  uint8_t device_id = 1;
};

unsigned long currentMillis;
unsigned long prevMillis;
unsigned long txIntervalMillis = 1000;

static uint32_t msg_counter = 0;

void setup() {
  Serial.begin(9600);
  Serial.println("Node starting..");
  radio.begin();
  radio.setDataRate( RF24_250KBPS );
  radio.setRetries(3, 5);
  radio.openWritingPipe(server_id);
}

void loop() {
  currentMillis = millis();
  if (currentMillis - prevMillis >= txIntervalMillis) {
    RF_MESSAGE msg;
    msg.message_id = msg_counter++;
    bool rslt;
    rslt = radio.write(&msg, sizeof(msg));
    Serial.print("Data Sent [");
    Serial.print(msg);
    if (rslt) Serial.println("] Acknowledge received");
    else Serial.println("] Tx failed");
    prevMillis = millis();
  }
}

I've moved assigning values to the struct RF_MESSAGE to inside the loop void, it might work better there.

I've entered some static numbers directly into struct RF_MESSAGE, is that ok?

I've returned the retry delay to a static number since you said it serves little purpose.

Im now getting a boatload of errors when I try to compile, so something is pretty busted.

D:\My Documents\Private\Arduino Stuff\Projects\Homesecurity_Node\Homesecurity_Node.ino: In function 'void loop()':

Homesecurity_Node:38:21: error: no matching function for call to 'HardwareSerial::print(RF_MESSAGE&)'

     Serial.print(msg);

                     ^

In file included from C:\Program Files (x86)\Arduino\hardware\arduino\avr\cores\arduino/Stream.h:26:0,

                 from C:\Program Files (x86)\Arduino\hardware\arduino\avr\cores\arduino/HardwareSerial.h:29,

                 from C:\Program Files (x86)\Arduino\hardware\arduino\avr\cores\arduino/Arduino.h:233,

                 from sketch\Homesecurity_Node.ino.cpp:1:

C:\Program Files (x86)\Arduino\hardware\arduino\avr\cores\arduino/Print.h:65:12: note: candidate: size_t Print::print(const __FlashStringHelper*)

     size_t print(const __FlashStringHelper *);

            ^~~~~

C:\Program Files (x86)\Arduino\hardware\arduino\avr\cores\arduino/Print.h:65:12: note:   no known conversion for argument 1 from 'RF_MESSAGE' to 'const __FlashStringHelper*'

C:\Program Files (x86)\Arduino\hardware\arduino\avr\cores\arduino/Print.h:66:12: note: candidate: size_t Print::print(const String&)

     size_t print(const String &);

            ^~~~~

C:\Program Files (x86)\Arduino\hardware\arduino\avr\cores\arduino/Print.h:66:12: note:   no known conversion for argument 1 from 'RF_MESSAGE' to 'const String&'

C:\Program Files (x86)\Arduino\hardware\arduino\avr\cores\arduino/Print.h:67:12: note: candidate: size_t Print::print(const char*)

     size_t print(const char[]);

            ^~~~~

C:\Program Files (x86)\Arduino\hardware\arduino\avr\cores\arduino/Print.h:67:12: note:   no known conversion for argument 1 from 'RF_MESSAGE' to 'const char*'

C:\Program Files (x86)\Arduino\hardware\arduino\avr\cores\arduino/Print.h:68:12: note: candidate: size_t Print::print(char)

     size_t print(char);

            ^~~~~

C:\Program Files (x86)\Arduino\hardware\arduino\avr\cores\arduino/Print.h:68:12: note:   no known conversion for argument 1 from 'RF_MESSAGE' to 'char'

C:\Program Files (x86)\Arduino\hardware\arduino\avr\cores\arduino/Print.h:69:12: note: candidate: size_t Print::print(unsigned char, int)

     size_t print(unsigned char, int = DEC);

            ^~~~~

C:\Program Files (x86)\Arduino\hardware\arduino\avr\cores\arduino/Print.h:69:12: note:   no known conversion for argument 1 from 'RF_MESSAGE' to 'unsigned char'

C:\Program Files (x86)\Arduino\hardware\arduino\avr\cores\arduino/Print.h:70:12: note: candidate: size_t Print::print(int, int)

     size_t print(int, int = DEC);

            ^~~~~

C:\Program Files (x86)\Arduino\hardware\arduino\avr\cores\arduino/Print.h:70:12: note:   no known conversion for argument 1 from 'RF_MESSAGE' to 'int'

C:\Program Files (x86)\Arduino\hardware\arduino\avr\cores\arduino/Print.h:71:12: note: candidate: size_t Print::print(unsigned int, int)

     size_t print(unsigned int, int = DEC);

            ^~~~~

C:\Program Files (x86)\Arduino\hardware\arduino\avr\cores\arduino/Print.h:71:12: note:   no known conversion for argument 1 from 'RF_MESSAGE' to 'unsigned int'

C:\Program Files (x86)\Arduino\hardware\arduino\avr\cores\arduino/Print.h:72:12: note: candidate: size_t Print::print(long int, int)

     size_t print(long, int = DEC);

            ^~~~~

C:\Program Files (x86)\Arduino\hardware\arduino\avr\cores\arduino/Print.h:72:12: note:   no known conversion for argument 1 from 'RF_MESSAGE' to 'long int'

C:\Program Files (x86)\Arduino\hardware\arduino\avr\cores\arduino/Print.h:73:12: note: candidate: size_t Print::print(long unsigned int, int)

     size_t print(unsigned long, int = DEC);

            ^~~~~

C:\Program Files (x86)\Arduino\hardware\arduino\avr\cores\arduino/Print.h:73:12: note:   no known conversion for argument 1 from 'RF_MESSAGE' to 'long unsigned int'

C:\Program Files (x86)\Arduino\hardware\arduino\avr\cores\arduino/Print.h:74:12: note: candidate: size_t Print::print(double, int)

     size_t print(double, int = 2);

            ^~~~~

C:\Program Files (x86)\Arduino\hardware\arduino\avr\cores\arduino/Print.h:74:12: note:   no known conversion for argument 1 from 'RF_MESSAGE' to 'double'

C:\Program Files (x86)\Arduino\hardware\arduino\avr\cores\arduino/Print.h:75:12: note: candidate: size_t Print::print(const Printable&)

     size_t print(const Printable&);

            ^~~~~

C:\Program Files (x86)\Arduino\hardware\arduino\avr\cores\arduino/Print.h:75:12: note:   no known conversion for argument 1 from 'RF_MESSAGE' to 'const Printable&'

exit status 1
no matching function for call to 'HardwareSerial::print(RF_MESSAGE&)'

So whats the verdict, doc? :smiley:

Serial.print doesn't know anything about your struct, you will need to print the components one at a time.

wildbill:
Serial.print doesn't know anything about your struct, you will need to print the components one at a time.

One little-known, but slick, feature of the Print class is that you can "teach" it to print your struct or class by having it inherit from the Printable Class.

I did see some people postfixing it to the struct instead though, is that important, or is that just personal preference?

packing the structure specifies that it needs to have the smallest possible alignment

On some architectures (eg 32 bits ESP, MKR) compilers like to see data aligned on adresses that are multiples of 4 bytes. so if your struct looks like this

struct RF_MESSAGE {
  uint8_t device_id;
  uint32_t message_id;
  uint32_t data;
}

you might end up having 3 empty bytes after device_id and then the message_id. the sizeof(RF_MESSAGE) will be 12 and that means that in order to send the structure out, you need to send 12 bytes.

Now if you put the device_id at the end, your structure might still (depends on compiler) be 12 bytes as it will keep the padding but when you send the data out you know that you can only send 9 bytes (in little endian architectures). Your radio transmission becomes 25% faster.

This is important in slow bandwidth networks for example or long range systems where the payload needs to be small (12 bytes for SIGFOX for example)

The notion of architectures also come into play as compilers can optimize things and shifting fields around to optimize memory and data fitting on multiple bytes might be little endian or big endian --> You want to be aware if that as when you get the payload on the other side you expect the bits and bytes in a specific place.

I've entered some static numbers directly into struct RF_MESSAGE, is that ok?

with

struct __attribute__((packed)) RF_MESSAGE {
  uint32_t message_id;
  uint32_t data = 1;
  uint8_t device_id = 1;
};

It's fine; You actually defined default values for any variables of that type.
message_id would be 0 if this is a global variable (as they are set to 0 for you by the compiler), or undefined (whatever is on the stack at that address) if it is defined as a local variable.

Ah, so with those adjustments were just condensing the payload so its more continuous and with less pad bloat, so transmission wont have to work through useless blanks. Sweet, efficiency!

I'm not sure how I can extract individual data from the struct though.

Ive got

struct __attribute__((packed)) RF_MESSAGE {
  uint32_t message_id;
  uint32_t data = 1;
  uint8_t device_id = 1;
};

And I would like to print data to serial, how would one go about doing that?
Serial.print(RF_Message.data); wont work, neither will Serial.print(data); or Serial.print(RF_MESSAGE[1]);
@gfvalvo mentioned it can be inherited from another library, but I don't know how to do that.
Should I just create the variables individually and then cast them into the struct?
Its a little roundabout, but its easy atleast.

Use msg.data

Use msg.data

If I may say so, it Seems that reading a couple tutorials on the C language basics would go a long way in making your life much easier....

C language tutorials are usually long and chewy and rarely stick, so I prefer to learn along the way. I gain more from deconstructing some functional code than I do grinding a manual. Struct was a new one to me, so I had a little difficulty understanding its logic. But with msg.data that issue is handled, so on to the next problem;

warning: invalid conversion from 'const byte* {aka const unsigned char*}' to 'uint64_t {aka long long unsigned int}' [-fpermissive]

   radio.openWritingPipe(server_id);

It is referring to const byte server_id[5] = {'R', 'x', 'A', 'A', 'A'};
radio.openWritingPipe wants a super long unsigned integer for some reason? I can give it the raw byte format by changing it to const uint64_t server_id = 5278414141;, but that would make it a little harder to edit. Shouldn't assigning server_id as byte already do that automatically?

You can read this about structures

J-M-L:
You can read this about structures

Oh, thats a bit more fleshed out than the explanation on the arduino playground page, thanks.

Sokonomi:
But with msg.data that issue is handled, so on to the next problem;

warning: invalid conversion from 'const byte* {aka const unsigned char*}' to 'uint64_t {aka long long unsigned int}' [-fpermissive]

radio.openWritingPipe(server_id);

Please post the complete program.

...R