I'd like to share the diaper logger that I created when my daughter was born. This is my entry to the Home Automation monthly contest.
I was looking for a project to work on and my wife said "I just need help with the baby". So naturally, I set about creating this beauty. Anyone who has had a newborn can understand what sleep deprivation can do to a person. Having a newborn also means spending a lot of time talking about the specific contents and timing of a diaper; as they can be an indicator of the health of the baby. Having just myself and my wife we quickly found that keeping up with the baby was very difficult, and when we would take over 'shifts' for each other, we would sometimes forget to communicate when the last diaper was given. There had to be an easier way...
I think I slammed this entire project together in about two weeks. It uses and Arduino ProMini along with an ESP-01 wifi module. There is a very simple three-button interface to log what kind of diaper was found. An RGB LED gives feedback to the user. When the buttons are pressed, the Arduino wakes up, connects to the ESP-01, establishes WIFI connectivity and opens a TCP port to my server. It sends a message over containing the diaper status, the goes back into deep sleep mode. During each stage, an RGB LED will glow to show the user what it is up to. The server is my computer in the basement running a C# program which is part of my larger home automation system. It logs the diaper messages to a SQL database. I also run Apache web server for a web interface from a computer or smartphone. The web interface provides much expanded functionality like the ability to view diapers from the last two days in a list, and a another section for logging each feeding - how much food, what kind of food, and what time.
I fittingly used a baby food container as the enclosure, and added three arcade buttons from Adafruit.
This was my first battery powered project, which was a really fun learning experience. I wanted to see how low I could get to extend the life of the system. I think I succeeded here as I was able to get through one baby on one set of batteries. I think it was about 170 button presses over maybe six months.
The Hardware
I chose the combination of two microcontrollers for a few reasons. I used an ESP-01 wifi module and an Arduino ProMini 3.3V 8MHz version. The ESP-01 is very cheap at about $5 and can add a WIFI interface using a simple serial port. It sadly lacks enough IO for what I wanted, and since I was already sleep deprived watching a newborn, I didn't want to mess around with learning to program a new board (although I have since learned it would have been pretty easy; and I could have used a Huzzah if it was available at the time). The Arduino ProMini was picked because it seemed well suited for a low power application. It is also inexpensive at about $10. The 8 MHz version of the ProMini uses less power than the 16 MHz version. The 3.3V board means it works easier with the ESP-01 which is also 3.3v logic level. It comes with an onboard voltage regulator which means the batteries plug right into it; but does not feature a USB to Serial like the Uno - instead an FTDI header is used to program it. Fortunately since this extra FTDI hardware is not on the board, it also consumes less power.
To run the ESP module, the Arduino uses a transistor to cut power on and off as required. When the Arduino validates the button push (SW debounce), it turns on power to the ESP-01; links up and sends the message, then cuts power back off. This way there is zero current drain from that module. I also tri-state the Arduino output to turn off the transistor; so there is no power required to hold it off.
There isn't much else for hardware other than some biasing resistors and decoupling capacitors.
Outputs: LEDs were on pins 3,5, and 6. The ESP-01 power was pin 2 of the Arduino ProMini.
Inputs: The three buttons were on pins 10, 11, and 12.
There was also the serial connection between the Arduino and the ESP which was simply TX --> RX and RX <-- TX.
Power savings
As noted above, I wanted to extend battery life. I spent some time researching what could be done for low power states. I ran Blink sketch to flash the onboard LED and measured the current to get a baseline. It was 7.8 mA with the LED ON and 4 mA while the LED was off. Keep that number in mind - 4mA just to have the Arduino stuck in a Delay() and doing literally nothing else. If you remember the Blink code, this uses a simple Delay() command to pause the code. I then tried turning my external RGB LED on and off (just using one of the lights). I used a NPN 2N3904 transistor with a 220 ohm base resistor and a 220 ohm resistor on the LED and ran Blink again. It came in at 18.4 mA when the LED was lit - so the external LED uses quite a bit more power than the little on-board LED.
There are a few tricks that can be done on this chip to save power. Here are some that I played with, and potential traps.
- Low Power Library
- #include <LowPower.h> to give easy access to useful low power modes.
- Disable unused peripherals of the chip
- ADCSRA = 0; disables ADC (analog to digital). I did this and saved 0.15 mA
- Some chips can disable I2C and SPI buses
- Disable the serial port. I tested with this and found 0.07 mA savings was possible; but I ended up not doing this since I was debugging with the serial port.
- Remove the power indicator LED from the board. Save 0.3 mA.
- Use PWM for the LED instead of just leaving it on
- As noted above, it was over 18 mA to run one single external LED. I used AnalogWrite() to PWM the pins instead
- AnalogWrite(100) got power down to 8.9 mA - a very significant power savings!
- AnalogWrite(25) got power down to 5.3 mA; but starting to push the limits in the LED being bright enough to see. I used PWM with various values on the Red, Green, and Blue LED to get a yellowish color as well.
- As noted above, it was over 18 mA to run one single external LED. I used AnalogWrite() to PWM the pins instead
- Change the clock prescaler.
- This was an interesting one. It has a huge potential to save power, but with the significant drawback that it changes many things which depend on the clock... So my PWM setting from before became manifested into a visibly blinking light when I divide the clock down too far. It also messes up the serial port. So in my code, I ended up changing clock settings on the fly depending on what I'm trying to do. For example, after a successful logging of data, I fade the green light on and off for a while. The microcontroller isn't doing anything else at this point other than fading the light, so I divide the clock as slow as I can get it. I don't have to worry about serial communication to the ESP module or anything other of the drawbacks.
- CLKPR = 0x00; = 3.82 mA at rest (delay) (This is the same as the baseline from above)
- CLKPR = 0x01; = 2.47 mA at rest; 3.67 mA active (delay)
- CLKPR = 0x02; = 1.49 mA at rest; 2.7 mA active (delay) (power down Sleep was still .11 mA) (I used this one in my code; the next level got too weird...)
- CLKPR = 0x03; = 1.00 mA at rest; 2.21 mA active (delay) - PWM was very noticeably affected here via the light looked to be blinking) (power down Sleep was still .11 mA)
- CLKPR = 0x04; = 0.72 mA at rest; 1.94 mA active (delay) (power down Sleep was still .11 mA)
- The clock prescalers values are 0,1,2,3, and 4 which correspond to dividing down by 1 (no scaling), 2 (half the clock speed), 4, 8, and 16.
- This was an interesting one. It has a huge potential to save power, but with the significant drawback that it changes many things which depend on the clock... So my PWM setting from before became manifested into a visibly blinking light when I divide the clock down too far. It also messes up the serial port. So in my code, I ended up changing clock settings on the fly depending on what I'm trying to do. For example, after a successful logging of data, I fade the green light on and off for a while. The microcontroller isn't doing anything else at this point other than fading the light, so I divide the clock as slow as I can get it. I don't have to worry about serial communication to the ESP module or anything other of the drawbacks.
- Component choice & physical board layout
- I chose to use a transistor to cut power to the ESP-01 entirely rather than mess about putting it into any low power sleep mode. Since this would get activated only a few times per day (or less) and we already have a main microprocessor doing most the rest of the work, there wasn't a need to sleep the ESP. In retrospect, I think that the Arduino wastes time doing hard power-off then a cold boot each time on the ESP. I could potentially clean up the code to wake up the ESP faster so it can go back to sleep faster.
- LOW POWER SLEEP!
- This is far and away the most useful one. Using the command "LowPower.powerDown(SLEEP_FOREVER, ADC_OFF, BOD_OFF);" I was able to get the chip to go into one of its lowest power modes. It could only be woken by an interrupt (my buttons). It has no scheduled wake up period for anything. BOD_OFF is a command for disabling brown out detection. ADC_Off disables the ADC portion while in sleep mode. This managed to get the complete system to consume only 0.104 mA. Yes - a mere 100 microamps.
- There were a few other places where I used "LowPower.powerDown(SLEEP_1S, ADC_OFF, BOD_OFF);" to give a similar effect; but with a scheduled wake up after 1 second. This was used instead of a Delay(1000) command since it saves more power.
So the board went from a baseline of 4 mA consumed during a Sleep() to 0.104 mA. And the current consumption through the rest of the cycle was also reduced.
If a AAA battery is 1000 mAh, then I have 4000 mAh available. If sleeping, the system could run for a theoretical 38,461 hours or 4.39 years consuming 0.104 mA (**If my math is correct - someone please chime in if not!**). If I roughly guestimate that it takes 37 seconds (per my meter) for a complete button press cycle, and I average the current at about 100 mA for that 37 seconds; we get 1.02 mA consumed per button press; or roughly 4000 button presses. Even at 200 mA per button press that would be about 2 mA consumed so roughly 2000 button presses. My testing seems to point that my math is off by one or two orders of magnitude as I got roughly 1.5 years and 170 button presses, but there isn't a guarantee that the batteries were good to start with as I used them all through the test and development cycle.
This image shows the power readout which is demonstrated in the video at the end of the post.
The Software
Here is the very unpolished code. Below the code there is a flowchart summary of what its doing.
#include <LowPower.h> const int ESP_Enable = 2; //ESP Enable pin to transistor - Active LOW (or tri-state to turn off) const int LED_Green = 3; //green LED pin - Active HIGH const int LED_Red = 5; //red LED pin - Active HIGH const int LED_Blue = 6; //blue LED pin - Active HIGH /* color Modes * Yellow - work in progress * Red - error * Green - success sending */ void setup() { pinMode(ESP_Enable, OUTPUT); //ESP Enable pinMode(LED_Green, OUTPUT); // green LED pinMode(LED_Red, OUTPUT); // red LED pinMode(LED_Blue, OUTPUT); // blue LED pinMode(10, INPUT); // button 3 - blowout pinMode(11, INPUT); // button 2 - dirty pinMode(12, INPUT); // button 1 - wet Serial.begin(38400); Serial.write("Starting..."); analogWrite(LED_Green, 25); //turn on green LED delay(200); LowPower.powerDown(SLEEP_1S, ADC_OFF, BOD_OFF); //sleep 1 sec ADCSRA = 0; //disable ADC to save power //CLKPR = 0x80; // tell Atmega we want to change clock speed //CLKPR = 0x00; // set divider - changes PWM and Serial timing as well. Can't use to power save... // enable interrupt for pins... pciEnable(); ESP_disable(); //be sure ESP is turned off Serial.println(" finished setup"); analogWrite(LED_Green, 0); //turn off green LED delay(200); LowPower.powerDown(SLEEP_1S, ADC_OFF, BOD_OFF); //sleep 1 sec } void pciSetup(byte pin) // set up interrupts for each pin (This is called from Setup function) { *digitalPinToPCMSK(pin) |= bit (digitalPinToPCMSKbit(pin)); // enable pin PCIFR |= bit (digitalPinToPCICRbit(pin)); // clear any outstanding interrupt PCICR |= bit (digitalPinToPCICRbit(pin)); // enable interrupt for the group } void pciEnable(){ pciSetup(10); // Port B 2 pciSetup(11); // Port B 3 pciSetup(12); // Port B 4 } void pciDisable() // set up interrupts for each pin (This is called from Setup function) { PCMSK1 = 0; // Atmel datasheet 13.2.7 PCMSK1 – Pin Change Mask Register 1 //disable pinChange interrupts PCICR = 0; //disable pin change interrupt register PCIFR = 0; //disbale pin change interript flags } bool readPins = false; void loop() { LowPower.powerDown(SLEEP_FOREVER, ADC_OFF, BOD_OFF); if(readPins) checkInputs(); } ISR (PCINT0_vect) // handle pin change interrupt for D8 to D13 { pciDisable(); // disable interrupts //Serial.write("~"); //debug digitalWrite(3,digitalRead(10)); digitalWrite(5,digitalRead(11)); digitalWrite(6,digitalRead(12)); if(readButtons() >0) readPins = true; //if one button is high, trigger to read fully pciEnable(); // re-enable interrupts } void ESP_disable(){ //turn off wifi module pinMode(ESP_Enable, INPUT); //set as Input (to enable tri-state) digitalWrite(ESP_Enable, HIGH); //turn off ESP (Via tri-state) } void ESP_enable(){ // turn on wifi module //analogWrite(LED_Green, 25); //analogWrite(LED_Red, 25); //set LED to yellow via red/green PWM combo pinMode(ESP_Enable, OUTPUT); //set as output (disable tri-state) digitalWrite(ESP_Enable, LOW); //turn on ESP LowPower.powerDown(SLEEP_1S, ADC_OFF, BOD_OFF); //sleep 1 sec Serial.begin(115200); Serial.setTimeout(2000); Serial.print("AT+CIOBAUD=38400"); //change baud to 38400 if not already. Needed for ProMini which doesnt support 115200 Serial.write('\r'); //cr Serial.write('\n'); //nl delay(300); //wait for serial message to be fully written LowPower.powerDown(SLEEP_1S, ADC_OFF, BOD_OFF); //sleep 1 sec Serial.begin(38400); const String setup[] = { "AT+RST", "AT+CWMODE=1", "AT+CWJAP=\"SSID\",\"Password\"", "AT+CIPMUX=1", "AT+CIPSTART=4,\"TCP\",\"192.168.1.n\",1234" }; bool success = true; for (int i = 0; i < 5; i++) { Serial.print(setup[i]); Serial.write('\r'); //cr Serial.write('\n'); //nl success = ESP_WaitString("OK"); //wait for the OK message } if(success){ //LED to Green //analogWrite(LED_Red, 0); //analogWrite(LED_Green, 25); //green light on } else{ //analogWrite(LED_Green,0); //analogWrite(LED_Red, 25); //red light on } } int readButtons(){ int button = 0; if(digitalRead(12)) button = 1; //wet else if(digitalRead(11)) button = 2; //dirty else if(digitalRead(10)) button = 3; //blowout return button; } int sendFailures = 0; void checkInputs() { digitalWrite(LED_Red, LOW); digitalWrite(LED_Green, LOW); digitalWrite(LED_Blue, LOW); pciDisable(); //diable interrupts //Serial.printSSIDins"); int button = readButtons(); if (button > 0) { //if they changed... delay(30); //debounce /* D12 = wet D11 = dirty D10 = wet & dirty */ button = readButtons(); //read again if (button > 0) { // now we really know what changed. TODO: masking all except pins 10, 11, 12 analogWrite(LED_Green, 35); analogWrite(LED_Red, 40); //light should be yellowish with red + green //Serial.println("Good press"); //delay(200); ESP_enable(); //turn ON ESP byte data[] = {0x51, button, 0, 0, 0, 0}; //0x51 = Diaper command. //was B 1: changed pins; B 2: last read Serial_SendBytes(data, 6); ESP_disable(); //turn off ESP //fade the light on or off for(int i=0; i< 10; i++){ if(sendFailures < 1) { fadePin(LED_Green); } else { fadePin(LED_Red); } } } else{ //Serial.println("didn't get button press"); //delay(200); } } readPins = false; //LowPower.powerDown(SLEEP_8S, ADC_OFF, BOD_OFF); //sleep 8 sec digitalWrite(LED_Green, LOW); digitalWrite(LED_Red, LOW); digitalWrite(LED_Blue, LOW); pciEnable(); } bool ESP_WaitString(String reply) //the reply is the message we're looking for to be returned from the ESP { unsigned long enterTime = millis(); //remember when we came in the loop bool result = false; String message = ""; while (!result) { message += Serial.readString(); checkConnection(); //turn light red if problems happen if (message.indexOf(reply) != -1) { sendFailures = max(0, sendFailures - 1); //for each good response, decriment the failed message counter. result = true; //if we find the messsage } if (millis() > enterTime + 20000) { sendFailures ++; Serial.println("Timeout waiting for message"); //timeout and exit the loop Serial.println("Message Buffer: "); Serial.print(message); break; } } Serial.print("result is: "); Serial.println(result); return result; //send result back to calling code } void checkConnection(){ if(sendFailures >3){ digitalWrite(LED_Green, LOW); //off green light analogWrite(LED_Red, 25); //on red light } if(sendFailures>3){ for(int i = 0; i < 5; i++){ fadePin(LED_Red); } asm volatile (" jmp 0"); //reset } } void Serial_SendBytes(byte buf[], int len) { //calc checksum and add byte chk = 0; byte packet[9] = { 0xF1, buf[0], buf[1], buf[2], buf[3], buf[4], buf[5], 0, 0xF2 }; for (int i = 0; i < 7; i++) { chk ^= packet[i]; //XOR all the bytes to get our checksum } packet[7] = chk; //add in our checksum Serial.print("AT+CIPSEND=4,9"); Serial.write('\r'); //cr Serial.write('\n'); //nl //ESP_WaitString(">"); delay(1000); Serial.write(packet, 9); //send the packet Serial.write('\r'); //cr Serial.write('\n'); //nl ESP_WaitString("OK"); } void fadePin(int ledPin){ digitalWrite(LED_Green, LOW); digitalWrite(LED_Red, LOW); digitalWrite(LED_Blue, LOW); CLKPR = 0x80; // tell Atmega we want to change clock speed CLKPR = 0x02; // set divider int fadeMin = 0; int fadeMax = 40; int delayTime = 4; //ms for delay (not including the clock divider) for (int fadeValue = fadeMin ; fadeValue <= fadeMax; fadeValue += 1) { analogWrite(ledPin, fadeValue); delay(delayTime); } for (int fadeValue = fadeMax ; fadeValue >= fadeMin; fadeValue -= 1) { analogWrite(ledPin, fadeValue); delay(delayTime); } CLKPR = 0x80; // tell Atmega we want to change clock speed CLKPR = 0x00; // set divider }
Code summary:
1. In the Setup() function, the Arduino does the normal things of configuring inputs, outputs, and enabling interrupts. It also executes a routine which enables to ESP-01 and sets it up for joining the wifi network. Finally it finishes by cutting power to the ESP and going into a deep sleep mode.
2. The Loop() function has only two lines of code. The first one puts the chip into a deep powerdown sleep mode. Then, since the three buttons are set up with Pin Change Interrupts, then the interrupt handler will be triggered on a button press and set a flag. The second line of the loop function will see this flag then execute the function to log the data.
void loop() { LowPower.powerDown(SLEEP_FOREVER, ADC_OFF, BOD_OFF); if(readPins) checkInputs(); }
3. For logging the data, an RGB LED is turned yellow while the ESP is being powered on. It then joins the network using the "AT+" commands. A TCP connection is established and a data packet is sent over. When successful, the LED then glows on and off a green light. If it fails, then the red LED will glow.
4. Once #3 is complete, the LED is extinguished and the ESP has power cut. The Arduino returns to deep sleep via the first line in the Loop().
Here is the website which displays the diapers on the right hand side. The "notes" column shows a period "." for all entries done via the Smart Dirty button. This is PHP script which is hitting the SQL database and doing some very basic formatting.
And finally - here is a video of it running!
https://www.youtube.com/watch?v=50VtZYnvi6c
Thanks and good luck to everyone on this!
Top Comments