As part of my plan for world domination, I have a pair of barebones 328s and each has a HC-SR04 and a nRFL01+. Let's call one of them the TX and the other the RX.
The TX transmits a packet to the RX, and then the TX immediately triggers its HC-SR04 (which only has the transmit transducer...I removed the receive transducer). Immediately after receiving the packet, the RX triggers its HC-SR04 (which only has the receive transducer...I removed the TX transducer). The two transducers are pointed at each other.
The RX then reports the distance measured by its HC-SR04. It is remarkably accurate, as shown in the following image:

I found this result surprising, because I figured that both of the HC-SR04s needed to be triggered almost exactly at the same time in order to report the correct distance. For example, if they were only 1 ms off, there would be a 34 cm error (340 m/s x 0.001s=0.34m). It seemed unlikely that they would be triggered sufficiently simultaneously due to differences in the durations of the radio.write and radio.available code.
Update: as reported in post #6, there is an almost negligible difference (36 micro-seconds) in the durations of those functions, so my concerns were not valid.
I decided to determine the difference in time between when "sonar.ping" was called by the RX and TX. To do that, I connected the uC grounds and pin 4, and added some timing code.
After a half-dozen tests, it was clear that the RX was triggering its HC-SR04 about 3 milliseconds before the TX was triggering its HC-SR04. Here's an example calculation:

So, it seems there ought to be a huge error due to that three millisecond difference, but there isn't. I'm stumped and would appreciate help in understanding why it works.
TX code:
// TX test
#include <SPI.h>
#include "RF24.h"
#include <NewPing.h>
//RF
const boolean radioNumber = 1; // TX=1
const byte enablePin = 7; // CE
const byte selectPin = 8; // CS
const byte addresses[][6] = {
"1Node", "2Node"};
// HC-SR04
const byte ECHO_PIN = A5;
const byte TRIGGER_PIN = A4;
const unsigned int MAX_DISTANCE = 500; // 5 meters = approx 16 ft
// other
byte timePin = 4;
byte ledPin = 9;
unsigned long startTime,txTime,sendTime;
// objects
RF24 radio(enablePin, selectPin); //Set up nRF24L01 radio on SPI bus plus CE and CS pins
NewPing sonar(TRIGGER_PIN, ECHO_PIN, MAX_DISTANCE); // NewPing setup of pins and maximum distance.
//***********************************
void setup() {
Serial.begin(115200);
Serial.println(F("TX"));
pinMode(timePin, OUTPUT);
digitalWrite(timePin, HIGH); // this pulls pin at RX uC high
startTime=micros(); // record the time here (and at the RX uC)
sendTime=startTime; // nRF xmits "sendTime" contents
pinMode(ledPin, OUTPUT);
blinkOnce(1500);
radio.begin();
radio.setChannel(108);
if (radioNumber) {
radio.openWritingPipe(addresses[1]);
radio.openReadingPipe(1, addresses[0]);
}
else {
radio.openWritingPipe(addresses[0]);
radio.openReadingPipe(1, addresses[1]);
}
radio.enableDynamicAck(); // oddly, this is req'd for "no ACK" writes
radio.stopListening();
}
//***********************************
void loop() {
Serial.println(F("Now sending"));
byte rWrite = radio.write( &sendTime, sizeof(unsigned long), true ); // "true" = no ACK (for future multicast)
txTime=micros();
byte dist = sonar.ping_cm();
blinkOnce(100);
sendTime=txTime; // next loop through, send txTime
delay(4000);
} // Loop
//***********************************
void blinkOnce(int dur) {
digitalWrite(ledPin, HIGH);
delay(dur);
digitalWrite(ledPin, LOW);
delay(dur);
}
RX code:
//RX test
// libs
#include <SPI.h>
#include "RF24.h"
#include <NewPing.h>
// RF
const boolean radioNumber = 0; // RX=0
const byte enablePin = 7; // CE
const byte selectPin = 8; // CS
const byte addresses[][6] = {
"1Node", "2Node"};
// HC-SR04
const byte ECHO_PIN = A5;
const byte TRIGGER_PIN = A4;
const unsigned int MAX_DISTANCE = 500; // 5 meters = approx 16 ft
// other
const byte timePin = 4;
const byte ledPin = 9;
unsigned long startTime,rxTime;
// objects
RF24 radio(enablePin, selectPin); // set up nRF24L01 radio on SPI bus plus CE and CS pins
NewPing sonar(TRIGGER_PIN, ECHO_PIN, MAX_DISTANCE); // NewPing setup of pins and maximum distance.
//***********************************
void setup() {
Serial.begin(115200);
Serial.println(F("RX"));
pinMode(ledPin, OUTPUT);
blinkOnce(1500);
// start RX uC first, then
// wait for TX to pull pin high
// and record start time at both uC
while (!digitalRead(timePin)) { // this will be off by one "while" loop
startTime=micros();
}
Serial.print("rx start time: ");
Serial.println(startTime);
radio.begin();
radio.setChannel(108);
if (radioNumber) {
radio.openWritingPipe(addresses[1]);
radio.openReadingPipe(1, addresses[0]);
}
else {
radio.openWritingPipe(addresses[0]);
radio.openReadingPipe(1, addresses[1]);
}
radio.startListening();
}
//***********************************
void loop() {
unsigned long gotTime;
if (radio.available()) {
rxTime=micros(); // save time RX starts ping
byte dist = sonar.ping_cm();
radio.read( &gotTime, sizeof(unsigned long) );
blinkOnce(100);
Serial.print("gotTime: ");
Serial.print(gotTime);
Serial.print(" 1st=tx startTime; 2nd=txTime for 1st ping");
Serial.print(" rxTime: ");
Serial.println(rxTime);
Serial.print(" Ping: ");
Serial.print(dist);
Serial.println("cm");
}
} // Loop
//***********************************
void blinkOnce(int dur) {
digitalWrite(ledPin, HIGH);
delay(dur);
digitalWrite(ledPin, LOW);
delay(dur);
}
