LowPowerLab Forum

Hardware support => Moteino => Topic started by: jrial on December 13, 2015, 06:48:49 PM

Title: Bidirectional communication (receiving after sending) issue [solved]
Post by: jrial on December 13, 2015, 06:48:49 PM
Hey,

I am working on a project involving wireless room thermostats and am hitting a bit of a snag. Part of the design involves the gateway sending a struct back, so the thermostat can update its screen accordingly.

The problem I'm facing is that while the gateway correctly receives the data from the thermostat, and replies to the message, the thermostat never seems to receive the reply.

I stripped out all code to do with the temperature reading, battery voltage measurement, dynamic node IDs etc... so I could test the simple send/receive loop (and also in preparation for this post, so there's as little irrelevant code in there as possible). The code I tested with is as follows:

Thermostat:
#include <RFM69.h>
#include <SPI.h>

#define NODEA       1
#define NODEB       2
#define NETWORKID   98
#define FREQUENCY   RF69_433MHZ
#define KEY         "SOMERANDOMSTRING"
#define LED         9
#define SERIAL_BAUD 115200
#define ACK_TIME    50  // # of ms to wait for an ack

RFM69 radio;
bool requestACK=true;
bool promiscuousMode = false; //set to 'true' to sniff all packets on the same network

typedef struct {
    char        sensorId[16];   // ID of the sensor; this is the node's ID card on the netwwork
    float       temp;           // Temperature
    bool        low_batt;       // Low battery indicator
} tempStruct;
tempStruct reportData;

typedef struct {
    char        curDate[10];    // To display the date on the thermostat nodes
    char        curTime[5];     // To display the time on the thermostat nodes
    float       targetTemp;     // Target temperature in thermostat node's room
    bool        burner;         // Burner is on (water is being heated)
    bool        valve;          // Valve to thermostat node's room open
} gwStruct;
gwStruct returnData;

void setup() {
    Serial.begin(SERIAL_BAUD);
    radio.initialize(FREQUENCY,NODEA,NETWORKID);
    radio.encrypt(KEY);
    radio.promiscuous(promiscuousMode);
    char buff[50];
    sprintf(buff, "\nTransmitting at %d Mhz...", FREQUENCY==RF69_433MHZ ? 433 : FREQUENCY==RF69_868MHZ ? 868 : 915);
    Serial.println(buff);
    sprintf(buff, "Encryption key: %s", KEY);
    Serial.println(buff);
    // radio.sleep();
    strncpy(reportData.sensorId , "1122334455667788", 16);
    reportData.temp = 22.5f;
    reportData.low_batt = false;
    Blink(LED,3);
}

void loop() {
    // Send
    Serial.print("Sending to node: ");
    Serial.println(NODEB);
    if (radio.sendWithRetry(NODEB, (const void*)(&reportData), sizeof(reportData))) {
        Serial.println("Ack received");
    }
    else {
        Serial.println("Nothing...");
    }
    if (radio.receiveDone()) {
        Blink(LED,3);
        Serial.println("Received reply");
        if (radio.DATALEN != sizeof(gwStruct)) {
           Serial.print("Invalid gwStruct received, not matching gwStruct struct!");
       }
        else {
            returnData = *(gwStruct*)radio.DATA;
            Serial.print("Date: ");
            Serial.println(returnData.curDate);
            Serial.print("Time: ");
            Serial.println(returnData.curTime);
            Serial.print("Temp: ");
            Serial.print(reportData.temp);
            Serial.print("/");
            Serial.println(returnData.targetTemp);
            Serial.print("Burn: ");
            Serial.println(returnData.burner ? "On" : "Off");
            Serial.print("Valv: ");
            Serial.println(returnData.valve ? "On" : "Off");
        }
        if (radio.ACKRequested()) {
            radio.sendACK();
        }
    } else {
        Serial.println("No reception yet...");
    }
    delay(1000);
}

void Blink(byte PIN, int DELAY_MS)
{
    pinMode(PIN, OUTPUT);
    digitalWrite(PIN,HIGH);
    delay(DELAY_MS);
    digitalWrite(PIN,LOW);
}


Gateway:
#include <RFM69.h>
#include <SPI.h>

#define NODEA       1
#define NODEB       2
#define NETWORKID   98
#define FREQUENCY   RF69_433MHZ
#define KEY         "SOMERANDOMSTRING"
#define LED         9
#define SERIAL_BAUD 115200
#define ACK_TIME    50  // # of ms to wait for an ack

RFM69 radio;
bool requestACK=false;
bool promiscuousMode = false; //set to 'true' to sniff all packets on the same network

typedef struct {
    char        sensorId[16];   // ID of the sensor; this is the node's ID card on the netwwork
    float       temp;           // Temperature
    bool        low_batt;       // Low battery indicator
} tempStruct;
tempStruct inData;

typedef struct {
    char        curDate[10];    // To display the date on the thermostat nodes
    char        curTime[5];     // To display the time on the thermostat nodes
    float       targetTemp;     // Target temperature in thermostat node's room
    bool        burner;         // Burner is on (water is being heated)
    bool        valve;          // Valve to thermostat node's room open
} gwStruct;
gwStruct outData;

void setup() {
    Serial.begin(SERIAL_BAUD);
    radio.initialize(FREQUENCY,NODEB,NETWORKID);
    radio.encrypt(KEY);
    radio.promiscuous(promiscuousMode);
    char buff[50];
    sprintf(buff, "\nListening at %d Mhz...", FREQUENCY==RF69_433MHZ ? 433 : FREQUENCY==RF69_868MHZ ? 868 : 915);
    Serial.println(buff);
    sprintf(buff, "Encryption key: %s", KEY);
    Serial.println(buff);
    strncpy(outData.curDate, "2015-12-08", 10);
    strncpy(outData.curTime, "21:41", 5);
    outData.burner = false;
    outData.valve = false;
    Blink(LED,3);
}

void loop() {
    strncpy(outData.curDate, "1970-01-01", 10);
    strncpy(outData.curTime, "12:00", 5);
    outData.targetTemp = 22.5f;
    outData.burner = true;
    outData.valve = true;
    if (radio.receiveDone()) {
        Blink(LED, 3);
        Serial.println("Received data");
        if (radio.DATALEN != sizeof(tempStruct)) {
            Serial.print("Invalid tempStruct received, not matching tempStruct struct!");
        }
        else {
            inData = *(tempStruct*)radio.DATA; //assume radio.DATA actually contains our struct and not something else
            Serial.print(" nodeId=");
            Serial.print(radio.SENDERID);
            Serial.print(" sensorId=");
            Serial.print(inData.sensorId);
            Serial.print(" temp=");
            Serial.print(inData.temp);
            Serial.print(" batt=");
            Serial.print(inData.low_batt ? "Low" : "OK");
        }

        delay(3);
        if (radio.ACKRequested()) {
            radio.sendACK();
        }
        Serial.println();
        Serial.print("Sending to node: ");
        Serial.println(NODEA);
        if (radio.sendWithRetry(NODEA, (const void*)(&outData), sizeof(outData))) {
            Serial.println("Ack received");
        }
        else {
            Serial.println("nothing");
        }
        Serial.println("End mock Transmission");
    }
}


void Blink(byte PIN, int DELAY_MS)
{
    pinMode(PIN, OUTPUT);
    digitalWrite(PIN,HIGH);
    delay(DELAY_MS);
    digitalWrite(PIN,LOW);
}


From the serial monitor, I see that the gateway appears to work correctly:
Listening at 433 Mhz...
Encryption key: SOMERANDOMSTRING
Received data
nodeId=1 sensorId=1122334455667788 temp=22.50 batt=OK
Sending to node: 1
nothing
End mock Transmission
Received data
nodeId=1 sensorId=1122334455667788 temp=22.50 batt=OK
Sending to node: 1
nothing
End mock Transmission
etc...


However, the "nothing" already indicates no ACK ever makes it back, and indeed, the serial monitor for the thermostat node shows the following:
Transmitting at 433 Mhz...
Encryption key: SOMERANDOMSTRING
Sending to node: 2
Ack received
No reception yet...
Sending to node: 2
Ack received
No reception yet...
etc...


I already ruled out things like external circuitry messing with the whole setup; these tests were performed with two bare moteinos with nothing connected to them.

I have no idea what's going on. The ACK is clearly received by the thermostat, but the following message never seems to make it through. I experimented with adding delays between the sendWithRetry call and the receiveDone call, just in case I was trying to receive too quickly after sending (a quick glimpse at the datasheet seems to confirm that there's a delay for transitioning between TX and RX modes, which seems logical and I already expected something similar). I tried adding a small delay between the sendAck on the gateway and the sendWithRetry as well. Neither worked. For good measure, I reversed the roles of thermostat and gateway node, to rule out any manufacturing errors wich would lead to a node being able to send but not receive, but as expected, this far fetched theory didn't fan out either. Besides, the ACK made it back, and that's just a packet like any other.

I'm obviously doing something wrong, but I have no idea what. Can someone point me in the right direction?

As an aside: from what I read on the site, the ACK is just one bit in the radio data, and the rest can be filled with actual  data. How is this done? Ideally I'd like to send back the response and ACK in one packet (migh have to set a bigger ACK WAIT value to compensate for the host doing its thing and sending over serial). But I don't see anything in the library that seems to support this.
Title: Re: Bidirectional communication (receiving after sending) issue
Post by: TomWS on December 13, 2015, 07:47:33 PM
If your far end is sending with retry, then it is best to ACK on receive as soon as you can.  This rule applies to both ends of the link.

Spending time serial printing data BEFORE ACK is bound to mess up the timing.  In general, call receiveDone(), if it returns a result, then save the data (senderID, DATA, DATALEN, etc) in your own variables (they'll be lost once you ACK) and Ack right away before you do anything with the incoming stuff.

And, yes, you can return 'data' with an ACK.  Look at the sendACK() protoype and you'll see that you can include a data packet with the ACK.  The downside is that you can not get an ACK on that packet.

Tom
Title: Re: Bidirectional communication (receiving after sending) issue
Post by: Sergegsx on December 14, 2015, 12:07:49 AM
Regardings Tom's recommendation, please check this link: https://lowpowerlab.com/forum/index.php/topic,1454.0.html
He already helped me coding it.
Title: Re: Bidirectional communication (receiving after sending) issue
Post by: jrial on December 14, 2015, 03:18:55 AM
Quote from: TomWS on December 13, 2015, 07:47:33 PM
If your far end is sending with retry, then it is best to ACK on receive as soon as you can.  This rule applies to both ends of the link.

Spending time serial printing data BEFORE ACK is bound to mess up the timing.  In general, call receiveDone(), if it returns a result, then save the data (senderID, DATA, DATALEN, etc) in your own variables (they'll be lost once you ACK) and Ack right away before you do anything with the incoming stuff.

And, yes, you can return 'data' with an ACK.  Look at the sendACK() protoype and you'll see that you can include a data packet with the ACK.  The downside is that you can not get an ACK on that packet.

Tom

TomWS,

Thanks for the reply, but the problem is not with the ACK; the ACK comes through fine.

But thanks for the advice. I was already wondering whether it would be a good idea to do the host communication before sending everything back with the ACK or not, and since you pointed it out I went back and looked at the timings... The communication will be too unreliable if the node needs to perform DB lookups etc, so I'm just gonna send the ACK and then process the data and send a reply separately.

Still, the problem with the reply packet never being received remains unresolved.
Title: Re: Bidirectional communication (receiving after sending) issue
Post by: jrial on December 14, 2015, 07:47:41 AM
Ah, sorry, I see where the confusion comes from; I mentioned an ACK never arriving in my first post, but what I meant was that the ACK never arrives back, since the other node never seems to receive the data packet in the first place, so there's nothing to ACK.

Here's the interaction diagram, with the results in the right column:










Node A: ThermostatNode B: GatewayStatus
Send sensor dataOK
Receive sensor dataOK
Send ACKOK
Receive ACKOK
Send screen update dataOK?
Receive screen update dataFAIL
Send ACKN/A
Receive ACKN/A
Title: Re: Bidirectional communication (receiving after sending) issue
Post by: Felix on December 14, 2015, 01:33:48 PM
I can almost bet it's got something to do with those delay(1000);
That's a no-no when you want to make sure you receive at any time. It's like a blackout when nothing happens. In fact the radio may receive a packet if it's in RX mode, but it may not have a change to reply in time for the other waiting node.
Also for some reason I don't like the nested if (receiveDone()) {... sendWithRetry()  ... }
It may work but I would try to restructure that to have clear separation of receiving and sending code.
Title: Re: Bidirectional communication (receiving after sending) issue
Post by: jrial on December 14, 2015, 06:53:42 PM
Bingo, nailed it!

For those struggling with a similar issue, here's the solution.

As I suspected, it had to do with the loop in my node 1 (thermostat). If you look at the original code, you see I send something, process the ACK, and then immediately check for a second packet, the actual response. Well, guess what, the response ain't that quick, so what happens is the loop rolls all the way to the end, does its delay, and then immediately sends again, expecting another ACK. The original reply came in during that delay, but sendWithRetry will request and receive an ACK, which is of course simply a packet with the ACK bit set to high. This packet flushes whatever was in the radio RX buffer in the first place, so of course I'm never going to read that response!

So what's the solution? Simple: the thermostat only needs to send once every minute, so let's do just that. We use millis() to store the time of last check-in, and in the loop we check whether 60 seconds have passed since the last send. If not, don't bother sending; I don't need sub-minute granularity on a room thermostat; that's overkill and only serves to drain the battery.

As for waiting for the second packet: well, we give the gateway a generous second to do its thing and send something back. If we still haven't got anything after a second, bugger that, we're not going to get a reply anyway. Stop wasting energy and take a nap for a minute or so, then try again.

The gateway code remains completely unchanged.

The result on the serial monitor:
Transmitting at 433 Mhz...
Encryption key: SOMERANDOMSTRING
Sending to node: 2
Ack received
No reception yet...
Received reply
Date: 1970-01-0112:00
Time: 12:00
Temp: 22.50/22.50
Burn: On
Valv: On
Goodnight, sweet prince!
Sending to node: 2
Ack received
No reception yet...
Received reply
Date: 1970-01-0112:00
Time: 12:00
Temp: 22.50/22.50
Burn: On
Valv: On
Goodnight, sweet prince!


Note that every successful reception is preceded by a message "No reception yet". I took care to print it only once, but if I hadn't, this would've been repeated quite a few times. This illustrates the problem in the original code: the reply packet takes a good while longer to arrive than a single pass through loop().

There are still some conceptual errors in the code. But it's late, so I'll fix that up tomorrow and post my working code so that others facing similar issues can learn from my mistakes.

Thanks for all the replies. Much appreciated.

ps: Felix, that sendWithRetry within the receiveDone is intentional: I only want to reply to a node when it actually reports in. It'll be asleep for most of the rest of the time, so no point in sending stuff while it's obviously not listening anyway. :)


[EDIT] Fixed the code. Here's the result:
#include <RFM69.h>
#include <SPI.h>
#include <LowPower.h>

#define NODEA       1
#define NODEB       2
#define NETWORKID   98
#define FREQUENCY   RF69_433MHZ
#define KEY         "SOMERANDOMSTRING"
#define LED         9
#define SERIAL_BAUD 115200
#define ACK_TIME    50          // # of ms to wait for an ack
#define SLEEP_LOOPS 8           // How many 8s periods to sleep before entering loop() again?

RFM69 radio;
bool requestACK=true;
bool promiscuousMode = false;   // Set to 'true' to sniff all packets on the same network

unsigned long lastSend = 0;     // When did we last send a temperature measurement to the node?
bool failNotified = false;      // To ensure we print "no reply yet" only once.

typedef struct {
    char        sensorId[16];   // ID of the sensor; this is the node's ID card on the netwwork
    float       temp;           // Temperature
    bool        low_batt;       // Low battery indicator
} tempStruct;
tempStruct reportData;

typedef struct {
    char        curDate[10];    // To display the date on the thermostat nodes
    char        curTime[5];     // To display the time on the thermostat nodes
    float       targetTemp;     // Target temperature in thermostat node's room
    bool        burner;         // Burner is on (water is being heated)
    bool        valve;          // Valve to thermostat node's room open
} gwStruct;
gwStruct returnData;

void setup() {
    Serial.begin(SERIAL_BAUD);
    radio.initialize(FREQUENCY,NODEA,NETWORKID);
    radio.encrypt(KEY);
    radio.promiscuous(promiscuousMode);
    char buff[50];
    sprintf(buff, "\nTransmitting at %d Mhz...", FREQUENCY==RF69_433MHZ ? 433 : FREQUENCY==RF69_868MHZ ? 868 : 915);
    Serial.println(buff);
    sprintf(buff, "Encryption key: %s", KEY);
    Serial.println(buff);
    radio.sleep();
    strncpy(reportData.sensorId , "1122334455667788", 16);
    reportData.temp = 22.5f;
    reportData.low_batt = false;
    Blink(LED,3);
    lastSend = millis();
}

void loop() {
    // Send
    Serial.print("Sending to node: ");
    Serial.println(NODEB);
    if (radio.sendWithRetry(NODEB, (const void*)(&reportData), sizeof(reportData))) {
        Serial.println("Ack received");
    }
    else {
        Serial.println("Nothing...");
    }
    lastSend = millis();
    failNotified = false;
    while (millis() - lastSend < 1000) {
        if (radio.receiveDone()) {
            Blink(LED,3);
            Serial.println("Received reply");
            if (radio.DATALEN != sizeof(gwStruct)) {
               Serial.println("Invalid gwStruct received, not matching gwStruct struct!");
            }
            else {
                returnData = *(gwStruct*)radio.DATA;
                Serial.print("Date: ");
                Serial.println(returnData.curDate);
                Serial.print("Time: ");
                Serial.println(returnData.curTime);
                Serial.print("Temp: ");
                Serial.print(reportData.temp);
                Serial.print("/");
                Serial.println(returnData.targetTemp);
                Serial.print("Burn: ");
                Serial.println(returnData.burner ? "On" : "Off");
                Serial.print("Valv: ");
                Serial.println(returnData.valve ? "On" : "Off");
                if (radio.ACKRequested()) {
                    radio.sendACK();
                }
            }
            break;
        } else if (!failNotified) {
            Serial.println("No reception yet...");
            failNotified = true;
        }
    }
    radio.sleep();
    Serial.println("Zzzzz");
    Serial.flush();
    for (int i=0; i<SLEEP_LOOPS; i++) {
        LowPower.powerDown(SLEEP_8S, ADC_OFF, BOD_OFF);
    }
}

void Blink(byte PIN, int DELAY_MS)
{
    pinMode(PIN, OUTPUT);
    digitalWrite(PIN,HIGH);
    delay(DELAY_MS);
    digitalWrite(PIN,LOW);
}
Title: Re: Bidirectional communication (receiving after sending) issue
Post by: Felix on December 14, 2015, 08:19:28 PM
Good work, congrats  8)