Spent the past few weeks hacking on this and finally got it working! A stock Motieno with LoRa radio running from a 1F, 12V capacitor bank and checking for commands every 2 seconds. By the looks of things it should last ~5 days on the capacitors alone.
The first step was to get CAD working. Fortunately the latest
RadioHead library supports it
out of the box.
Next, I needed a way to reliably detect channel activity. My first attempt looked like this:
void loop() {
// use the CAD function to check for an incoming message
if (driver.isChannelActive()) {
// attempt to recieve the incoming packet
uint8_t len = sizeof(buf);
if (manager.recvfromAckTimeout(buf, &len, 1000)) {
// handle the command
Serial.write(buf, len);
}
}
// sleep for 8 seconds before checking again
driver.sleep();
LowPower.powerDown(SLEEP_8S, ADC_OFF, BOD_OFF);
}
The problem I discovered with this approach was that the transmitter divides it's time between transmitting and listening for ACKs. This means that there's only a 50% chance of detecting channel activity, assuming you push the timeout as low as possible. (25ms according to my testing.)
My next attempt was to modify the RadioHead library to use a very long (500ms) preamble for standard packets and a much shorter preamble (5ms) for ACKs. I set my sleep cycle to do CAD checks in pairs spaced 60ms apart, and then sleep 8 seconds before trying again. That way at least one of the two checks would be guaranteed to run during a preamble.
void loop() {
// use the CAD function to check for an incoming message
if (driver.isChannelActive()) {
for (uint8_t i=0; i < 2; i++) {
// attempt to recieve the incoming packet
uint8_t len = sizeof(buf);
if (manager.recvfromAckTimeout(buf, &len, 1000)) {
// handle the command
Serial.write(buf, len);
// don't check again
break;
}
driver.sleep();
LowPower.powerDown(SLEEP_60MS, ADC_OFF, BOD_OFF);
}
}
// sleep for 8 seconds before checking again
driver.sleep();
LowPower.powerDown(SLEEP_8S, ADC_OFF, BOD_OFF);
}
This worked, but battery life was much less than expected. Measuring the current consumption on my scope showed that a full wakeup and CAD check only consumed 0.00027 Joules, so that's definitely not the problem.
It turns out that the CAD check is not 100% reliable and sometimes gives false positives. When this happened, RadioHead was keeping the Moteino + RF module awake for a full 1000ms waiting for a packet that would never come. I calculated that I couldn't afford to spend more than 20ms waiting for an incoming packet before sleeping.
For reasons I still don't understand, I couldn't get the timeout below 250ms without causing RX errors. I reduced the preamble time, TX timeout, and tweaked every other parameter I could think of but never got it working reliably.
Since I was still an order of magnitude beyond my target I decided to try working with a thinner abstraction. My next attempt used RHDatagram, which is really just a thin wrapper over the RH_RF95. When I want to reach a sleeping node, I send a barage of packets for 10 seconds without waiting for ACKs, greatly simplifying the node's RX logic. I've also reduced my sleep cycle to 2 seconds now that I have a better idea of what's drawing most of the power.
void loop() {
// use the CAD function to check for an incoming message
if (driver.isChannelActive()) {
// switch the radio to RX mode
driver.setModeRx();
// wait for the packet to come in.
LowPower.powerDown(SLEEP_15MS, ADC_OFF, BOD_OFF);
// check if anything has arrived
uint8_t len = sizeof(buf);
if (manager.recvfrom(buf, &len)) {
// immediately sleep the radio
driver.sleep();
// handle the command
Serial.write(buf, len);
}
}
// sleep for 8 seconds before checking again
driver.sleep();
LowPower.powerDown(SLEEP_2S, ADC_OFF, BOD_OFF);
}
And what do you know, it works wonderfully! This is my first Moteino project and I'm glad I finally got it working.