Mailbox Notifier Odd Behavior

Apologies, this is going to be a long post.

I am using an Arduino Pro Mini in conjunction with the Adafruit solar charger and Minty Boost for power. I have a 3.7V 4400mAh Li-ion battery for power when solar is not available. I am using an XBee (series 1) to send a character (0 or 1) to turn a light in my house on when the mail is delivered. There are also two LEDs mounted to the outside of the mailbox for external notification. The electronics are housed in an IP65 enclosure which is mounted inside the mailbox.

In order to save power I have it programmed so the Arduino puts the XBee to sleep when it’s not in use and then the Arduino puts itself to sleep. The Arduino is woken up via external interrupt, it then wakes the XBee, runs through the program and then everything goes back to sleep. I use two interrupts, one for delivery and one for retrieval, both are triggered by a respective reed switch.

On the bench everything works perfectly. Even when I first get the mailbox outside and mounted on the post it all works perfectly. I first noticed issues when I was testing and the temperature started to drop. We were getting to sub-freezing temps. At these temps I found that things didn’t work quite as expected - the delivery would almost always work, but the retrieval would almost always fail. Timing also seemed to be off, the XBee would not stay awake as long as it should. Sometimes it would wake up and then immediately go off. Other times it would stay on for much longer than it should.

I’m certain the issues I’m having are related to the cold temps. because if I played with it enough outside, ran through the delivery and retrieval sequences enough times to get things warmed up, then it would start to work. If I bring the mailbox back inside and give it a bit to warm up, everything starts to work again too. However, I’m not approaching temps below what the components are rated for. The lowest temps it’s been in are 4F and almost all of the components are rated to -40C. And now the temps have warmed up to the mid to high 30s (F) but I’m still seeing the issue.

The only thing that I can think of is that because the Arduino and XBee both sleep most of the time and draw so little power that they are able to get really cold and that then affects the operation. My feeling is that the crystal on the Arduino is getting to cold which is why I’m seeing the timing issues. Still the crystal is rated to -40C… I’ve also seen various Arduino projects that relegate it to being outside and have not heard any issues reported. The only difference is all those projects have the Arduino in a cyclic wake-sleep cycle which, I imagine, would keep the Arduino warmer than it would be just being asleep.

Anyway, just to be sure I’m hoping that someone could look over my code to insure that I don’t have anything in there that would be inducing the this behavior.

Thanks.

/**********************************************************************************************************************
Mailbox Notifier - Mailbox Side

This sketch was written to work as a Snail mail box delivery notification system. It uses to LEDs mounted to the mailbox
as local (to the mailbox) delivery notifications. It also sends out a serial character via a connected XBee modem. When 
the receiving XBee (set in a remote location like in your house) receives the character it either turns an indicator
light on or off.

For more information see - http://adambyers.com/2013/11/mailbox-notifier/ or ping adam@adambyers.com

All code (except external libraries and third party code) is published under the MIT License.

**********************************************************************************************************************/

#include <avr/sleep.h>

// States
#define S_sleep 1
#define S_process 2
#define S_deliver 3
#define S_retrieve 4

// Default start up state
int state = S_sleep;

// PIN Names
int DeliverSW = 2;
int RetrieveSW = 3;
int XBPower = 4;
int NotifyLED1 = 5;
int NotifyLED2 = 6;
int DomeLED = 7;

// Vars
bool delivery = false;
int transmitCount = 0;
int timesToTransmit = 20; // Number of times we will send out the ON or OFF signal out for the house notification. This can be reduced if the distance/interferace of the enviornmen is low
int XBeeWakeupDelay = 15000; // Number of milliseconds we wait after waking the XBee before using it
int XBeeTransmitDelay = 1500; // Number of milliseconds we wait between transmitions

void setup() { 
  // Set PINs as inputs or outputs
  pinMode(DeliverSW, INPUT);
  pinMode(RetrieveSW, INPUT);
  pinMode(XBPower, OUTPUT);
  pinMode(NotifyLED1, OUTPUT);
  pinMode(NotifyLED2, OUTPUT);
  pinMode(DomeLED, OUTPUT);
  
  // Enable internal resistors by setting the PINs as HIGH. This is so we don't have to use pesky external resistors
  digitalWrite(DeliverSW, HIGH);
  digitalWrite(RetrieveSW, HIGH);
  
  // Start your serial engines!
  Serial.begin(9600);  
}

void loop() { 
  switch(state) {
    
    case S_sleep:
      F_SleepyTime();
    break;
  
    case S_process:
      F_process();
    break;
  
    case S_deliver:
      F_deliver();
    break;
    
    case S_retrieve:
      F_retrieve();
    break;
  }
}

void F_SleepyTime() {
  attachInterrupt(0, F_Interrupt, RISING); // Attach the interrupt so that we can wake up when mail is delivered
  attachInterrupt(1, F_Interrupt, RISING); // Attach the interrupt so that we can wake up when mail is retrieved
  
  digitalWrite(XBPower, HIGH); // Put the XBee to sleep
  
  delay(100); // Slight delay for sanity
  
  set_sleep_mode(SLEEP_MODE_PWR_DOWN); // Most power savings in this mode
  
  sleep_enable(); // Enable sleep
  
  sleep_mode(); // Go to sleep
  
  sleep_disable(); // When woken up will continue to process from this point
 
  state = S_process; // Figure out what switch woke up the Arduino and act accordingly
} 

void F_Interrupt() {
// Empty funciton for interrupt
}

void F_process() { 
  // Functionally unnecessary but for good mesure we detach the interrupts while we are not using them
  detachInterrupt(0);
  detachInterrupt(1);
  
  if (digitalRead(DeliverSW) == HIGH){
    state = S_deliver;
  }
  
  if (digitalRead(RetrieveSW) == HIGH) {
    state = S_retrieve;
  } 
}
 
void F_deliver() {  
  if (delivery == false) {
    digitalWrite(XBPower, LOW); // Turn the radio on
    delay(XBeeWakeupDelay); // Give the radio some time to settle after waking up
    
    // For the sake of making sure the house notification gets its ON signal we send the ON command out, wait 1.5 seconds and then send it out again until transmitCount is equal to timesToTransmit
    while(transmitCount != timesToTransmit){  
      Serial.println("1"); // Send a character out the serial to turn on the house notification
      delay(XBeeTransmitDelay); // Give some time for the transmition to end before we send the next one
      transmitCount ++;
    }
    
    // Turn the external notification LEDs on
    digitalWrite(NotifyLED1, HIGH);
    digitalWrite(NotifyLED2, HIGH);
    
    transmitCount = 0; // Reset
    delivery = true;
    state = S_sleep;
  }
  
  // To make sure we conserve power we just go right back to sleep if the delevery door is opened after we have already detcted delivery
  else if (delivery == true) {
    state = S_sleep;
  }
}

void F_retrieve() {
  if (delivery == true) {
    // Turn off the external notification LEDs
    digitalWrite(NotifyLED1, LOW);
    digitalWrite(NotifyLED2, LOW);
   
    // Turn on the dome LED on untill the retrive door is closed so we can see inside the mailbox if it's dark outside
    while (digitalRead(RetrieveSW) == HIGH) {
      digitalWrite(DomeLED, HIGH);
    } 
    
    digitalWrite(DomeLED, LOW); // Turn the dome LED off
    
    digitalWrite(XBPower, LOW); // Turn the radio on
    delay(XBeeWakeupDelay); // Give the radio some time to settle after waking up before doing anything with it
    
    // For the sake of making sure the house notification gets its OFF signal we send the OFF command out, wait 1.5 seconds and then send it out again until transmitCount is equal to timesToTransmit
    while(transmitCount != timesToTransmit){  
      Serial.println("0"); // Send a character out the serial to turn off the house notification
      delay(XBeeTransmitDelay);
      transmitCount ++;
    }
    
    transmitCount = 0; // Reset
    delivery = false;
    state = S_sleep;
  }

  // To make sure we conserve power we just go right back to sleep if the the retrive door is opened when a delivery has not been detected
  else if (delivery == false) {
    state = S_sleep;
  }
}

I don’t think it accounts for your problem, but I think the way you’re using transmitCount is a bit strange. The logic appears to be intended to repeat the Serial.println() call 20 times and I think it will probably do that, but it relies on transmitCount having been left at zero last time it was used. A simple for loop would be a far more natural solution for this requirement.

There also seems to be some tautology at line 138 - if delivery is used as a boolean and it is not false then it can be assumed to be true and does not need to be tested again.

I don’t understand what concept delivery is meant to represent. You use it both for delivery and retrieval but with different logic to decide whether to sleep afterwards. Perhaps if you could explain the expected sequence of delivery and retrieval events and when the sleep is intended to occur in relation to them, it might make more sense.

Having said all that, if the behaviour seems correct when it’s warm and incorrect when it’s cold then first I’d do a sanity check of the inputs to make sure nothing was floating and all the connections were sound, and then I’d run a simple blink sketch and see whether the timing remained consistent after the hardware was put in a fridge.

Ok, I'll explain the what is expected to happen at each step...

Mail is delivered:

Delivery door is opened, triggering the interrupt, waking the Arduino. The Arduino sees that it's the delivery door that was triggered and runs the deliver function.

The XBee is woken up and the Arduino waits for 15 seconds before doing anything else.

The Arduino then sends a 1 out the serial which the XBee then transmits to turn the house light on. Because I am near the limit of the Xbee range I have it set to transmit the 1, wait 1.5 seconds and then transmits it again, it does this 20 times to insure that the signal is received. I use the var transmitCount to count how many times the signal was sent out when it reaches 20, we stop transmitting.

The external notification LEDs are turned on.

The transmitCount is then reset to 0 so that it can be used during the retrieve function.

This:

else if (delivery == true) {
    state = S_sleep;
  }

was put there to put the Arduino back to sleep if the delivery door was opened after delivery was already detected. You are correct that this would be redundant if we were not concerned with the Arduino sleeping. If this was not there the interrupt would wake the Arduino go to the deliver function and then sit there, never going back to sleep.

The delivery var is set to true so we know to run the retrieve function if the retrieval door is opened.

Mail is delivered:

Retrieval door is opened, triggering the interrupt, waking the Arduino. The Arduino sees that it's the retrieval door that was triggered and runs the retrieve function.

External LEDs are turned off.

Dome light is turned on and stays on (we loop here) until the retrieval door is closed.

Retrieval door is closed so we turn the dome LED off, turn the XBee on, wait 15 seconds and then transmit.

The transmit sequence is the same as during the delivery - only this time we send a 0 to turn off the light. This is why transmitCount was reset to 0 so we can use the var here to count how many time we've transmitted.

After the transmit sequence we reset transmitCount set delivery to false and then go back to sleep.

Here, the use of the boolean delivery is used to detect if the delivery sequence has run. If it has not we can just go back to sleep if the retrieval door is opened.

If I understand you correctly, delivery is true when mail has been delivered but not retrieved.

I can’t see anything in your code which would cause the behaviour you describe and I think you’re right to suspect a temperature related hardware issue. I can see several things in your code which IMO could be improved, but don’t expect them to cure the problem:

Detaching the interrupt in F_process() seems pointless and may be harmful if any race conditions exist in your code (I don’t see any).

As mentioned previously, I recommend getting rid of transmitCount and using a simple for loop to control the retransmission.

This code:

  if (delivery == false) {

(snip)
 
   delivery = true;
    state = S_sleep;
  }
  else if (delivery == true) {
    state = S_sleep;
  }

Could be simplified to:

  if (delivery == false) {

(snip)
 
   delivery = true;
  }
state = S_sleep;

Similarly in F_retrieve().

Using a finite state machine to trigger one action when one interrupt occurs and another action when another interrupt occurs seems Rube Goldbergian. Wouldn’t it be simpler to have the interrupt handler wake the processor and set/clear delivery and have loop send the status messages corresponding to delivery and go back to sleep? It feels as if you have two or three times more code here than strictly needed. But none of this explains why the timing is wrong when it’s cold.

Now that I look at it you are right about the state machine... probably to much for this but I used on in another project and just like the flow of it.

Yes, the delivery var is used to keep the delivery function from triggering more than once after mail has been delivered and to also only start the retrieval function if mail has been delivered.

I struggled with detaching the interrupts... I was not sure when or where to do it. Examples showed you detaching the interrupts in the interrupt function but doing that cause them to only work once. Glad to know my feeling was right, that in this case it's not necessary.

I suppose your issue with transmitCount is a matter of taste. I see how a for loop could be more elegant and eliminate code... but either would work.

Thanks for taking a look

rocketboy001: Now that I look at it you are right about the state machine... probably to much for this but I used on in another project and just like the flow of it.

If you are feeling adventurous, you could try re-writing this with two interrupt handlers that just set a flag, and a loop that just contains a 'for' loop and a sleep command, and then see which implementation you think is simpler to write and more likely to be correct. I'd expect that simple version to be less than a dozen lines of code. Every statement and every variable you eliminate is one less thing you have to write and one less place for bugs to live. If your sketch had been trivially simple (as it could have been) then it would have been possible to see at a glance that it was not a software issue. As it is, you and I and anyone else trying to guess what's going wrong will have had to trace through all your logic trying to work out ways that it might be going wrong. It's a truism in software development that you throw the first version away. With experience, you throw it away while it's still in your head. :)

KISS.

I re-wrote it last night, hopefully this is easier to follow.

In any case, since you didn’t think that the coding was the fault of the issues I was seeing I went ahead and swapped out the pro mini for another; grasping at this point. I’ll have to wait till later tonight or tomorrow morning to see if the issue pops up again since it spent the night inside.

Thanks again.

#include <avr/sleep.h>

// PIN Names
int DeliverSW = 2;
int RetrieveSW = 3;
int XBPower = 4;
int NotifyLED1 = 5;
int NotifyLED2 = 6;
int DomeLED = 7;

// Vars
bool delivery = false;
int transmitCount = 0;
int timesToTransmit = 10; // Number of times we will send out the ON or OFF signal out for the house notification. This can be reduced if the distance/interferace of the enviornmen is low
int XBeeWakeupDelay = 20000; // Number of milliseconds we wait after waking the XBee before using it
int XBeeTransmitDelay = 1500; // Number of milliseconds we wait between transmitions

void setup() { 
  // Set PINs as inputs or outputs
  pinMode(DeliverSW, INPUT);
  pinMode(RetrieveSW, INPUT);
  pinMode(XBPower, OUTPUT);
  pinMode(NotifyLED1, OUTPUT);
  pinMode(NotifyLED2, OUTPUT);
  pinMode(DomeLED, OUTPUT);
  
  // Enable internal resistors by setting the PINs as HIGH. This is so we don't have to use pesky external resistors
  digitalWrite(DeliverSW, HIGH);
  digitalWrite(RetrieveSW, HIGH);
  
  // Start your serial engines!
  Serial.begin(9600);  
}

void loop() { 
  digitalWrite(XBPower, HIGH); // Put the XBee to sleep
  delay(1000); // wait a second and then go to sleep
  F_sleep();
}

void F_sleep() {
  attachInterrupt(0, F_Interrupt, RISING); // Attach the interrupt so that we can wake up when mail is delivered
  attachInterrupt(1, F_Interrupt, RISING); // Attach the interrupt so that we can wake up when mail is retrieved
  
  delay(100); // Slight delay for sanity
  
  set_sleep_mode(SLEEP_MODE_PWR_DOWN); // Most power savings in this mode
  sleep_mode(); // Go to sleep
  sleep_disable(); // When woken up will continue to process from this point
  
  F_process(); // Figure out what switch woke up the Arduino and act accordingly
}
  
void F_Interrupt() {
}

void F_process() { 
  if (digitalRead(DeliverSW) == HIGH && delivery == false) {
    digitalWrite(XBPower, LOW); // Turn the radio on
    delay(XBeeWakeupDelay); // Give the radio some time to settle after waking up
    
    // For the sake of making sure the house notification gets its ON signal we send the ON command out, wait 1.5 seconds and then send it out again until transmitCount is equal to timesToTransmit
    for (int t=0; t != timesToTransmit; t++){
      Serial.println("1"); // Send a character out the serial to turn on the house notification
      delay(XBeeTransmitDelay); // Give some time for the transmition to end before we send the next one
    }
    
    digitalWrite(NotifyLED1, HIGH); // Turn the external notification LEDs on
    digitalWrite(NotifyLED2, HIGH); // Turn the external notification LEDs on
    
    delivery = true;
}
  
  if (digitalRead(RetrieveSW) == HIGH && delivery == true) {
    digitalWrite(NotifyLED1, LOW); // Turn off the external notification LEDs
    digitalWrite(NotifyLED2, LOW); // Turn off the external notification LEDs
   
    // Turn on the dome LED on until the retrive door is closed so we can see inside the mailbox if it's dark outside
    while (digitalRead(RetrieveSW) == HIGH) {
      digitalWrite(DomeLED, HIGH);
    } 
    
    digitalWrite(DomeLED, LOW); // Turn the dome LED off
    
    digitalWrite(XBPower, LOW); // Turn the radio on
    delay(XBeeWakeupDelay); // Give the radio some time to settle after waking up before doing anything with it
    
    // For the sake of making sure the house notification gets its OFF signal we send the OFF command out, wait 1.5 seconds and then send it out again until transmitCount is equal to timesToTransmit
    for (int t=0; t != timesToTransmit; t++){
      Serial.println("0"); // Send a character out the serial to turn on the house notification
      delay(XBeeTransmitDelay); // Give some time for the transmition to end before we send the next one
    }
    
    delivery = false;
  }
}

I would hazard a guess your battery is freezing. Storage temperature specification is not the same as operating.

Test the supply voltages while it is out in the mailbox.

When it was colder I did check the voltage and current draw and it was fine. The issue also occurred when thing warmed up, well above freeing but still cold. The charger also monitors the temperature to insure the batter is not charged if it's too cold or hot.

Perhaps worth checking again though.