User avatar
piglet
Posts: 911
Joined: Sat Aug 27, 2011 1:16 pm

Re: HOW-TO Build an Automatic Power Supply for the Pi

Mon Nov 30, 2015 11:07 am

Given the number of pi's out there, I'm sure there would be a market for this. I'd like one. Are you looking to get this out as a product, or will it stay an interesting thing you've built for yourself?

paulv
Posts: 558
Joined: Tue Jan 15, 2013 12:10 pm
Location: Netherlands

Re: HOW-TO Build an Automatic Power Supply for the Pi

Mon Nov 30, 2015 9:32 pm

Hi piglet,

Thank you for the interest. You are not the only one with this question, and many have already looked at this post. I do plan to build one or two more for myself, but I'm still refining the design while I'm learning about new features.

As an example, there are a few posts I contributed too about adding a start-stop button with an automatic shutdown, but I've not incorporated that into the automatic UPS design yet. I also purchased a couple of commercially available solutions, but have not yet found one I really like.

My major stumbling block at this moment is that I don't use schematic capture with the aim to turn it into a PCB, only to document my designs, and I've never learned to do a modern PCB layout (30+ year gap) That's on my bucket list for this coming spring as a project too, but it's competing with so many others...and then time. Lots of hobbies and interesting things to do, all competing for precious time.

If I ever get around turning this into a PCB, you'll see it appear here. In the meantime, stay tuned for updates and new designs.

Enjoy!
Paul

paulv
Posts: 558
Joined: Tue Jan 15, 2013 12:10 pm
Location: Netherlands

Re: HOW-TO Build an Automatic Power Supply for the Pi

Thu Dec 03, 2015 9:49 am

There have been some changes to the schematic since the previous version, so here is my latest one :
Automatic PS V3.1.png
Automatic PS V3.1.png (59.54 KiB) Viewed 3811 times
I have also updated the daemon controller (in /etc/init.d) so it checks for the presence of the "Hat", the board that interfaces the Pi to the UPS. If it's not found, the UPS program will not be started.

Code: Select all

#!/bin/sh

### BEGIN INIT INFO
# Provides: my_ps_service
# Required-Start: $remote_fs $syslog
# Required-Stop: $remote_fs $syslog
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Short-Description: This deamon controls my_app_daemon to handle an automatic
# Pi Power Supply with UPS
# Description: The App has code that will control the power supply (PS) to handle
# uninterrupted Boot and Shutdown windows when the main power is dropped.
# A software controlled push button controls the stopping of the App, or the reboot
# of the Pi
### END INIT INFO

# Call this shell script my_ups_service.sh
# This shell script will call my_app_daemon.py
# Copy this init script into /etc/init.d using
#       sudo cp ups_service.sh /etc/init.d/.
# Make sure the script is executable
#       sudo chmod 755 /etc/init.d/ups_service.sh
#
# At this point you should be able to start the Python script using the command
#       sudo /etc/init.d/ups_service.sh start
# Check its status with
#       sudo /etc/init.d/ups_service.sh status
# and stop it with
#       sudo /etc/init.d/ups_service.sh stop
#
# to install my_ps_service in the boot sequence :
# sudo update-rc.d ups_service.sh defaults
#
# to remove from the boot sequence :
# sudo update-rc.d ups_service.sh remove

# Change the next 3 lines to suit where the python script is and what you want to call it
DIR=/home/pi
DAEMON=$DIR/my_app_daemon.py
DAEMON_NAME=my_app_daemon

# make sure that my_app_daemon.py is executable and the the Python shebang is present
# chmod 755 test_simple_ups.py

# This next line determines what user the script runs as.
# Root generally not recommended but necessary if you are using the Raspberry Pi GPIO from Python.
DAEMON_USER=root

# The process ID of the script when it runs is stored here:
PIDFILE=/var/run/$DAEMON_NAME.pid

. /lib/lsb/init-functions

do_start () {
    # check to see if we can detect the UPS_HAT on port 22
    port22=""
    echo "22" > /sys/class/gpio/export
    echo "in" > /sys/class/gpio/gpio22/direction
    port22=`cat /sys/class/gpio/gpio22/value`
    echo "GPIO22 is $port22"
    # clean up
    echo "22" > /sys/class/gpio/unexport
    if [ $port22 -eq 1 ]; then
       log_daemon_msg "UPS HAT detected, starting system $DAEMON_NAME"
       start-stop-daemon --start --background --pidfile $PIDFILE --make-pidfile --user $DAEMON_USER --chuid $DAEMON_USER --startas $DAEMON
       log_end_msg $?
    else
        log_daemon_msg "NO UPS_HAT detected, NOT Starting system $DAEMON_NAME"
        echo ""
    fi
}
# we use the --background flag of start-stop-daemon to run our script
# in the background

do_stop () {
    log_daemon_msg "Stopping system $DAEMON_NAME"
    start-stop-daemon --stop --pidfile $PIDFILE --retry=TERM/10/KILL/5
    log_end_msg $?
}
# the --retry means that first of all a TERM -15 signal is sent
# to the process and then 10 seconds later it will check if the process is still there
# and if it is send a KILL -9 signal (which definitely does the job).
# if you catch the TERM-15 signal within the app, you can control the shutdown
# sequence and properly save all important data for a restart. You don't want
# to be caught unprepared for the Kill -9, if needed, extend the 10 seconds.

case "$1" in

    start|stop)
        do_${1}
    ;;

    restart|reload|force-reload)
        do_stop
        do_start
    ;;

    status)
        status_of_proc "$DAEMON_NAME" "$DAEMON" && exit 0 || exit $?
    ;;

    *)
        echo "Usage: /etc/init.d/$DAEMON_NAME {start|stop|restart|status}"
        exit 1
    ;;
esac

exit 0
I also made some changes to the "de luxe" version, the one I'm using myself.
Most notable with the previously posted code is the swap of the GPIO-4 and GPIO-7 pins.
This version has full scale logging capabilities for when the code runs as a daemon or will use print statements running in foreground during debug. All set with constants in the code.

Code: Select all

#!/usr/bin/env python2.7
#-------------------------------------------------------------------------------
# FileName:     my_app_daemon.py
# Purpose:      This program interfaces with the Pi Power & UPS hardware to
#               handle brown-outs and loss of the main power. The app will be
#               able to boot and shutdown without power issues, to protect the
#               SD card, and to preserve the running variables of the app.
#
#               This is the de-lux version, to be combined with your own app.
#
# Note:         All dates are in European format DD-MM-YY[YY]
#
# Author:       Paul Versteeg
#
# Created:      08-Dec-2012 and Feb-2015
#
#    This program is free software: you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation, either version 3 of the License, or
#    (at your option) any later version.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    To get a copy of the GNU General Public License
#    go to <http://www.gnu.org/licenses/>
#-------------------------------------------------------------------------------


import os
import RPi.GPIO as GPIO
from time import sleep
import signal
import subprocess
import sys
import logging
import logging.handlers
import traceback


#===============================================================================
# Global constants & variables

# debug and test constants
DEBUG = True
TRACE = True
REBOOT = True
SHUTDOWN = True
TERMINATE_2_OS = True
RUN_AS_DAEMON = True

# NOTE:
# The GPIO_cleanup function sets ALL (used or not) Ports back to the
# default state, which is input. In our case, this will remove Power to the Pi
# and it will become powerless without any control. Also note that if you use
# another program in parallel (concurrent) that also uses GPIO ports, a
# cleanup in either program will clean ALL GPIO ports, regardless!
GPIO_cleanup = False

# constants
__author__ = 'Paul Versteeg'
VERSION = "1.0d"
OFF = False
ON = True
if DEBUG :
    UPS_MODE = 20       # UPS provides power for a period of 20 seconds
else:
    UPS_MODE = 10*60    # 10 minutes in normal operation
                        # Beware, it may take 5-10 x longer to re-charge due
                        # to the 100mA trickle charge

# GPIO Ports
PWR_Gate_P = 17         # output GPIO-04
PWR_Sense_P = 4         # input  GPIO-17
PWR_Timer_P = 27        # output GPIO-27
PWR_Reboot_P = 2        # input  GPIO-02 (SDA with 1K8 pull-up)
System_Status_P = 3     # output GPIO-03 (SCL with 1K8 pull-up
Halt_State = 14         # output GPIO-14 (shows Halt State) not driven by this App

disable_button_action = False


# ==============================================================================
# there are a couple of directory structures used by this program
# all references to files are relative to the executing directory
# normally /home/pi
#
# set reference path to that of the location of the executed application
exec_path = os.path.abspath(os.path.dirname(__file__))

# here is where we put the shutdown flag
shutdown_file = exec_path+"/app_shutdown_ok"

# here is where we store the errors and warnings
log_file = exec_path+"/my_daemon.log"
# create log_rotate files as follows
log_rotate_file = log_file+".1"

# create the logger instance, it is used by write_log()
logger = logging.getLogger(__name__)

#===============================================================================



def init():
    '''
    Initializes a number of settings and prepares the environment
    before we start the main application.

    '''

    global sys_stat

    try:
        # Setup the logging environment
        #
        # ----- create the log file
        #
        if not os.path.isfile(log_file): # does the file exist?
            cmd = "touch "+log_file
            subprocess.call([cmd], shell=True)
            cmd = "chmod goa+w "+log_file
            subprocess.call([cmd], shell=True)
        #
        # ----- setup the main logging environment
        #
        logger.setLevel(logging.DEBUG)

        # Add the log message handler to the logger
        # you can use the following granulatity "when" to rotate:
        #    second (s)
        #    minute (m)
        #    hour (h)
        #    day (d)
        #    w0-w6 (weekday, 0=Monday)
        #    midnight
        # Here we use 7 days worth of log data
        handler = logging.handlers.TimedRotatingFileHandler(log_file,
                                                           when="h",
                                                           interval=1,
                                                           backupCount=7)
        #
        # logfiles can also be based on file size:
        # handler = logging.handlers.RotatingFileHandler(log_file,
        #                                               maxBytes=100000,
        #                                               backupCount=10,
        #                                               )

        # create the formatter, asctime is with milli seconds added
        formatter = logging.Formatter('%(asctime)s %(levelname)-8s %(message)s')

        # add formatter to handler
        handler.setFormatter(formatter)

        # add handler to logger
        logger.addHandler(handler)
        #
        # Can now start to use write_log()
        #
        #=======================================================================
        write_log("info", "my_app_daemon V{0} initialisation started\n".format(VERSION))
        #
        # create a timestamp file to record the starting time.
        cmd = "touch {0}/[email protected]".format(exec_path)
        subprocess.call([cmd], shell=True)

        #
        # ----- prepare the restart of the app
        #
        # did we properly terminate the last shutdown or exit?
        if os.path.isfile(shutdown_file): # does the file exist?
            write_log("boot", "Last Shutdown was OK")
            # can restart the app with the saved data
        else:
            write_log("warning", "Last Shutdown was NOT OK")
            # need to start from scratch, saved data is not reliable
        #
        # remove the file
        cmd = "rm -f {0}".format(shutdown_file)
        subprocess.call([cmd], shell=True)
        #
        # ----- setting up the GPIO Ports
        #
        write_log("boot", "Starting hardware interface setup")

        if DEBUG: GPIO.setwarnings(True)
        else: GPIO.setwarnings(False)

        # Use the Raspberry Pi BCM pins
        GPIO.setmode(GPIO.BCM)

        # This is the port that will trigger the Power Timer to control the
        # uninterrupted shutdown period
        # Setting the Port LOW now will stop the Watchdog Timer from firing
        # until we release it again
        GPIO.setup(PWR_Timer_P, GPIO.OUT, initial=GPIO.LOW)

        # This port must now be set HIGH to turn the PowerGate on within the
        # Timer period of 45 seconds, otherwise the Pi will become powerless
        # after the Timer runs out.
        GPIO.setup(PWR_Gate_P, GPIO.OUT, initial=GPIO.HIGH)

        # Main Power loss input, we will set this up as an interrupt further below
        GPIO.setup(PWR_Sense_P, GPIO.IN)

        # system Stop_App/Pi_Reboot button, the interrupt is defined below
        GPIO.setup(PWR_Reboot_P, GPIO.IN)

        # system status LED setup
        GPIO.setup(System_Status_P, GPIO.OUT)
        # We'll use Pulse Width Modulation to control it
        # start with a frequency of 0.5 Hz
        sys_stat = GPIO.PWM(System_Status_P, 0.5)
        # start the PWM, but with a duty cycle of 0; the LED is on with no flash
        sys_stat.start(0)

        # --- these definitions need to be after the port definitions,
        # --- otherwise the detections will not work!
        #
        # setup the event to detect a falling edge on the main power sense input
        GPIO.add_event_detect(PWR_Sense_P, GPIO.FALLING, callback=power_action, bouncetime=5000)
        #
        # setup the event detection thread for the Stop_App/Pi Reboot button
        GPIO.add_event_detect(PWR_Reboot_P, GPIO.FALLING, callback=button_action, bouncetime=5000)
        #
        # if we lost the main power during the boot process, we need to
        # check this here and take action.
        if (GPIO.input(PWR_Sense_P) == 0) :
            write_log ("debug", "Main power lost during boot process")
            power_action(PWR_Sense_P)
            # maybe power comes back during the UPS period, otherwise we'll
            # shutdown the Pi
        #
        # start the app hart-beat
        system_status(ON, 20, 0.5) # Normal Operation, 20% ON, 80% OFF @ 0.5 Hz
        #
        write_log("system", "UPS Supply setup is finished. Starting...\n")
        subprocess.call(['logger "UPS Supply setup is finished. Starting Main"'], shell=True)

    except Exception as e:
        subprocess.call(['logger "UPS: Unexpected Exception in init()"'], shell=True)
        traceback.print_exc(file=open(exec_path+"/my_daemon_errlog.txt","a"))
        print "error in init", e
        write_log("Error", "Unexpected Exception in init() : \n{0}".format(e))

        if GPIO_cleanup :
            # reset all used I/O pins back to normal (input)
            # NOTE, this will lower the PWR_Gate and the system will be powerless
            write_log("Warning", "GPIO channels will reset, Pi will become powerless!")
            GPIO.cleanup()
        #
        os._exit(1) # force the exit to the OS, sys.exit(1) will NOT do that properly!



def write_log(mode, event):
    '''
    Function to write various messages and errors to a log file.

    We can look at the information off-line while the daemon is running.
    Use tail -f logfile

    The various events the logger understands:
        - DEBUG
        - INFO
        - WARNING
        - ERROR
        - CRITICAL

    Using the logger function this way allows for much more freedom on what to do
    '''

    try:
        if TRACE and mode == "trace" :
            if RUN_AS_DAEMON == False:
                # if we run the program ourselves we can print to the console
                print event
            logger.info(event)
            return

        if mode == "info" or mode == "message" :
            logger.info(event)
            return

        if DEBUG and mode == "debug" :
            logger.debug(event)
            return

        if mode == "parms" or mode == "system" or mode == "boot" or mode == "mode":
            logger.debug(event)
            return

        if mode == "error" :
            logger.error(event)
            return

        if mode == "warning" :
            logger.warning(event)
            return

        if mode == "critical" :
            logger.critical(event)

    except Exception as e:
        traceback.print_exc(file=open(exec_path+"/my_daemon_errlog.txt","a"))
        logger.error("UPS: Write_log Exception", exc_info=True)
        # do not call write_log(), we will get into a loop
        return



def set_shutdown_flag():
    '''
    Function to set a flag if we did shutdown properly.

    It allows you to properly restart your app and continue, or to really
    re-initialize your app.
    '''
    write_log("trace", "set shutdown flag")

    cmd = "touch "+shutdown_file
    subprocess.call([cmd], shell=True)



def system_status(mode, duty_c=20, freq=0.5):
    '''
    Function to drive the status LED.

    parameters : mode [ON|OFF],
                 duty_cycle [0..100] 0=on, 100=off,
                 blinking frequency in Hz
    '''
    global sys_stat

    try:
        write_log("trace", "system_status Mode={0} Duty_Cycle={1}% Freq.={2}Hz".format(mode, duty_c, freq))

        if mode:
            sys_stat.ChangeDutyCycle(duty_c)
            sys_stat.ChangeFrequency(freq)
        else:
            sys_stat.ChangeDutyCycle(100) # LED is off

    except Exception as e:
        write_log("error", "system_status Exception : \n{0}".format(e))
        logger.error("UPS: System_status Exception", exc_info=True)
        return



def button_action(PWR_Reboot_P):
    '''
    This call_back function controls the pressing of the Reboot/Terminate button.

    The debounce does not always work, so we do it in software.
    The call_back function should not call itself again while we're processing
    the button press, so we use a traffic light.

    '''
    global disable_button_action

    if disable_button_action :
        # already processing...
        return
    else:
        disable_button_action = True
        process_button(PWR_Reboot_P)



def process_button(PWR_Reboot_P):
    '''
    This function controls the pressing of the Reboot/Terminate button.
    Based on a software timer, we either Reboot or terminate this program.

    When we reboot after the shutdown, a filecheck is performed.
    '''
    global sys_stat, disable_button_action

    try:
        write_log("warning", "Reboot/Terminate Action Detected")

        button_press_timer = 0

        # signal user that we've seen the button press
        # starting to flash the LED
        system_status(ON, 50, 1)

        while (GPIO.input(PWR_Reboot_P) == False) :
                write_log("debug", "reboot_button_action : Timer={0} Sec".format(button_press_timer))

                if button_press_timer > 15 :
                    # second thought or something went wrong
                    break

                if button_press_timer == 8 :
                    # Threshold for Halting program is reached
                    # flash at 10 Hz so the user can release if the Halt is wanted
                    system_status(ON, 50, 10)

                elif button_press_timer == 4 :
                    # Threshold for the Reboot
                    # flash at 4 Hz so the user can release if the Reboot is wanted
                    system_status(ON, 50, 4)

                button_press_timer += 1 # keep counting until button is released
                sleep(1) # 1 sec timer for button_press measurement

        # The button is released, figure out for how long
        write_log("debug", "button released : Timer={0} Sec".format(button_press_timer))

        # is press is <= 4 or > 15 : no action
        if (button_press_timer <= 4) or (button_press_timer > 15):
            write_log("trace", "Reboot button pressed, but too short/long")
            button_press_timer = 0
            system_status(ON, 20, 0.5) # system LED flashing back to normal
            disable_button_action = False
            return

        # is press for > 4 < 8 seconds : Reboot!
        if (button_press_timer > 4) and (button_press_timer < 8):
            write_log("trace", "System reboot by reset button")
            #
            # add code here to prepare the app for the restart
            # the reboot condition is also captured by sig_handler
            # keep that in mind
            #
            # if we get here, we can set the flag that we exited normally
            set_shutdown_flag()
            #
            if REBOOT :
                start_pwr_timer()
                # Don't release the power gate of the UPS to keep power on
                # this shutdown is actually a reboot and we do a file check as well
                subprocess.call(['shutdown -F -r now "System reboot by reset button" &'], shell=True)
                sleep(30) # prevent the system from doing things
            else:
                write_log("debug", 'shutdown -F -r now "System reboot by reset button"')
                system_status(ON, 20, 0.5) # system LED flashing back to normal
                disable_button_action = False
                write_log("debug", "REBOOT = False : returning to main")
            return

        if (button_press_timer >= 8) : # pressed for > 8 seconds : Terminate!
            write_log("trace", "Program will be terminated by reset button")
            #
            # add code here to prepare for a restart
            # the terminate condition is also captured by sig_handler
            # keep that in mind.
            #
            # if we get here, we can set the flag that we exited normally
            set_shutdown_flag()
            #
            start_pwr_timer()
            # Don't release the power gate of the UPS to keep power on
            #
            # terminate the program
            write_log("trace", "Terminating Program")
            # force the exit to the OS
            if TERMINATE_2_OS :
                os._exit(0)
            else:
                write_log("debug", "TERMINATE_2_OS is False : going back to main")
                system_status(ON, 20, 0.5) # system LED flashing back to normal
                return

    except Exception as e:
        write_log("error", "Unexpected Exception in reset_action() : \n{0}".format(e))
        subprocess.call(['logger "UPS: Unexpected Exception in reset_action()"'], shell=True)
        disable_button_action = False
        return



def start_pwr_timer():
    '''
    This function controls the Timer that turns the Power_Gate ON and controls
    the Watch_Dog at the same time.

    The Watch_Dog has been turned OFF in the init of this App by setting the
    Power_Timer pin LOW.

    This function will trigger the Timer with a negative edge, and it will run
    for about 45 Sec. Enough to keep the Power_Gate ON and supply guaranteed
    power for an uninterrupted shutdown operation.

    If the mains drops during this period, the batteries will power the Pi,
    so we're safe from power Brown Outs during this critical procedure.

    If the main power comes back while we are in the shutdown period, this App
    will miss the mains ON interrupt to keep the Power_Gate open, and the Pi
    will become powerless. The WatchDog will make sure that the Timer will fire
    and turn on the power to the Pi, so it can boot automatically.

    Setting the output HIGH will enable the Watch_Dog.
    '''
    try:
        write_log("trace", "Start UPS timer")
        # Start the UPS timer with a negative edge
        GPIO.output(PWR_Timer_P, GPIO.HIGH)
        sleep(0.01)
        GPIO.output(PWR_Timer_P, GPIO.LOW)
        # Timer has started, now turn on the Watch_Dog
        sleep(0.01)
        GPIO.output(PWR_Timer_P, GPIO.HIGH)

    except Exception as e:
        write_log("error", "Unexpected Exception in reset_action() : \n{0}".format(e))
        subprocess.call(['logger "UPS: Unexpected Exception in reset_action()"'], shell=True)
        return


def power_action(PWR_Sense_P):
    '''
    This function controls the Power to the Pi.

    If a loss of main power is detected, the supply acts as a UPS to see if it
    only was a glitch, if not, the Pi is shutdown.

    After the shutdown of the Pi, it will become powerless. The power supply
    will automatically turn the power on to let the Pi boot if the main
    power returns.
    '''
    try:
        write_log("warning", "power action -> Loss of Power Detected")

        # flash at 4 Hz so the user can see we have a loss of main power
        system_status(ON, 50, 3)

        sleep(1) # Let the voltage drop completely
        # we can play UPS for a while before we perform the shutdown.
        # if there was a power glitch, we'll let the Pi continue
        # if the mains is really out and we lost power, we need to shutdown the Pi.

        ups_timer = 1
        while (ups_timer < UPS_MODE) and (GPIO.input(PWR_Sense_P) == 0): # play UPS
            sleep(1) # check every second
            ups_timer += 1
        write_log("trace", "UPS mode finished : UPS={0} timer={1}".format(UPS_MODE, ups_timer))

        if ups_timer < UPS_MODE :
            # The power came back!
            sleep(1) # let the system settle a bit and check it again
            if GPIO.input(PWR_Sense_P) == 1 :
                write_log("trace", "Power came back on during UPS period")
                system_status(ON, 20, 0.5) # system LED flashing back to normal
                return

        write_log("trace", "Main power still off -> starting shutdown process")
        # show the user
        system_status(ON, 50, 6) # start flashing at 6 Hz
        #
        # add code here to handle a restart
        # sig_handler will catch this action too!
        #
        # start the shutdown process
        #
        # set the flag that we exited normally
        set_shutdown_flag()
        #
        # Start the shutdown process
        if SHUTDOWN:
            write_log("trace", "Start shutdown sequence!")
            #
            start_pwr_timer()
            #
            # Release the power gate of the UPS to let the 555 timer
            # control the power to the Pi until shutdown is completed.
            write_log("trace", "Release power gate and start shutdown sequence")
            GPIO.output(PWR_Gate_P, GPIO.LOW)
            #
            subprocess.call(['shutdown -hP now "System shutdown due to main power failure" &'], shell=True)
            # when the shutdown sequence has started, it cannot be stopped. If the power comes back on
            # in this period, the Pi will still be left powerless. In that case,
            # the PWR_Watchdog will restart the Pi.
            #
            sleep(60) # don't return, wait for the shutdown to happen
        else:
            write_log("trace", "SHUTDOWN = False: Start test of shutdown sequence")
            write_log("debug", 'shutdown -hP now "System shutdown due to main power failure" &')

            if GPIO_cleanup :
                # reset all used I/O pins back to normal (input)
                # NOTE, this will lower the PWR_Gate and the system will become powerless
                write_log("warning", "GPIO channels will reset, Pi will become powerless!")
                GPIO.cleanup()
            write_log("debug", "return to main")
            system_status(ON, 20, 0.5) # system LED flashing back to normal
            return

    except Exception as e:
        # in case that there is an exception, still force the shutdown!
        # if the shutdown flag has already been set, fine, otherwise, we'll have
        # to restart from scratch
        #
        write_log("error", "Unexpected Exception in power_action() : {0}, reboot".format(e))
        subprocess.call(['logger "UPS: Unexpected Exception in power_action(), reboot"'], shell=True)
        sleep(2)
        if SHUTDOWN :
            # Make sure we can finish the reboot without a power interruption
            start_pwr_timer()
            #
            # Release the power gate of the UPS to let the 555 timer
            # control the power to the Pi until shutdown is completed.
            write_log("trace", "Release power gate and start shutdown sequence")
            GPIO.output(PWR_Gate_P, GPIO.LOW)

            if GPIO_cleanup :
                # reset all used I/O pins back to normal (input)
                # NOTE, this will lower the PWR_Gate and the system will become powerless
                write_log("warning", "GPIO channels will reset, Pi will become powerless!")
                GPIO.cleanup()

            subprocess.call(['shutdown -F -r now "System reboot due to error in power_action()" &'], shell=True)
            sleep(60)
        else:
            write_log("debug", 'shutdown -F -r now "System reboot due to error in power_action()" &')
        return



def sig_handler (signum=None, frame = None):
    '''
    This function will catch the most important system signals, but NOT a shutdown!
    During debugging, we need to be able to stop the execution, but preserve the status
    so we can restart it again.

    This handler catches the following signals from the OS:
        SIGHUB = (1) SSH Terminal logout
        SIGINT = (2) Ctrl-C
        SIGQUIT = (3) ctrl-\
        IOerror = (5) when terminating the SSH connection (input/output error)
        SIGTERM = (15) Deamon terminate (deamon --stop): is coming from deamon manager
    However, it cannot catch SIGKILL = (9), the kill -9 or the shutdown procedure

    NOTE: if the termination takes longer than the standard start-stop daemon allows
    modify : start-stop-daemon --stop --pidfile $PIDFILE --retry=TERM/30/KILL/5
    send TERM with a 30 second wait (or use more) and then KILL with 5 seconds
    '''
    try:
        write_log("trace", "Sig_handler called with signal : {0}".format(signum))
        if signum == 1 :
            write_log("trace", "ignoring signal {0}".format(signum))
            return # 1:ignore SSH logout termination
        write_log("trace", "Sighandler is terminating the application")
        # start flashing the status LED so we will see what's going on
        system_status(ON, 50, 10)
        #
        # add code here to handle a restart
        #
        # set the flag that we exited normally
        set_shutdown_flag()
        #

        if GPIO_cleanup :
            # if we do a GPIO.cleanup, the Pi will become powerless right away!
            write_log("trace", "GPIO channels will reset, Pi will become powerless!")
            GPIO.cleanup()
        #
        write_log("trace", "Sig Handler is done, Exiting to shell\n\n")
        sleep(2)
        os._exit(1) # force the exit to the OS

    except Exception as e: # IOerror 005 when terminating the SSH connection
        write_log("error", "Unexpected Exception in sig_handler() : \n{0}".format(e))
        subprocess.call(['logger "UPS: Unexpected Exception in sig_handler()"'], shell=True)
        return



def main():
    '''
    The main daemon routine.

    After the initialisation, we start the control thread.

    We try to catch the most important os signals so we can terminate the program but preserve the integrity.
    Note: we cannot catch kill -9 or shutdown, so that will be handled in the deamon manager that starts
    this program at boot time.

    '''
    #
    init()

    write_log("trace", "Run as Daemon = {0}".format(RUN_AS_DAEMON))
    write_log("trace", "GPIO Version = {0}".format(GPIO.VERSION))
    write_log("trace", "Trace = {0}".format(TRACE))
    write_log("trace", "Debug = {0}".format(DEBUG))
    write_log("trace", "GPIO_cleanup = {0}".format(GPIO_cleanup))
    write_log("trace", "Reboot = {0}".format(REBOOT))
    write_log("trace", "Shutdown = {0}".format(SHUTDOWN))

    # setup a catch for the following signals: signal.SIGINT = ctrl-c
    for sig in (signal.SIGTERM, signal.SIGINT, signal.SIGHUP, signal.SIGQUIT):
        signal.signal(sig, sig_handler)


    try:

        while True :
            sleep(1)
            #
            # wait for a button press to terminate or shutdown
            #
            # watch out for a loss of the main power

    except Exception as e:
        write_log("error", "Python exception : {0}".format(e))
        # capture the traceback information causing the Python termination
        #
        # add code here to handle a restart
        #
        # set a flag if we exited normally
        set_shutdown_flag()
        # so we can restart the program again
        #
        # Make sure we can terminate properly
        start_pwr_timer()

        if GPIO_cleanup :
            # reset all used I/O pins back to normal (input)
            # NOTE, this will lower the PWR_Gate and the system will become powerless
            write_log ("warning", "GPIO channels will reset, Pi will become powerless!")
            GPIO.cleanup()
        #
        # Always clean-up the PWR_Sense port so the deamon can test for the presence of the UPS
        # and avoid loading when not.
        GPIO.cleanup(PWR_Sense_P)

        subprocess.call(['logger "Forcefully terminating with error"'], shell=True)
        write_log("error", "UPS: Forcefully terminating with error {0}".format(e))
        # exit en return an error code for this instance
        os._exit(1) # force the exit to the OS


if __name__ == '__main__':
    main()
Enjoy!

paulv
Posts: 558
Joined: Tue Jan 15, 2013 12:10 pm
Location: Netherlands

Re: HOW-TO Build an Automatic Power Supply for the Pi

Tue Dec 22, 2015 2:48 pm

With what I’ve learned a while ago about creating a GPIO pin that signals the Halt state of the Pi, I wanted to finally upgrade the automatic power supply with this feature as well.

At this moment, when the software decides to shutdown the Pi, the power will remain on until the Timer runs out that controls the power window. On a stock Pi Model B Rev 2, a normal shutdown or powerdown takes about 10 seconds. This means that power will continue to be on for another 35 seconds before it gets cut.

The reason I designed the Timer with this seemingly lengthy 45 second window is to account for processes that need to finish before the power gets cut. In my particular case, I want to log quite a lot of information, send out an email, and sync and unmount my connected drives. Linux will do the latter two during the shutdown too, but I would like to be in control myself, and make sure there is no lost data. All this takes time, and I wanted to be absolutely safe before the power gets cut.
So how do we implement and add the feature that will kill the power right after the Pi has been halted? Cutting the slack so to speak?

We need to initialize a GPIO pin that will change state when the Pi has reached the halt state.
The only reliable way of doing that, because of the power-up conditions, is to create an active high pin. This signal then needs to help cut the power. The easiest way is to drive the Reset signal, pin 4 of the 555 Timer section, low. It is normally pulled high slowly (470K & 470nF) after power-up to prevent false triggers, but if we drive it low, the Timer will be reset, and the Powerswitch will cut the power to the Pi.

Because we can only tolerate an active high GPIO pin, we need to use a level shifter to create an active low signal to drive the Reset pin of the Timer.

(you cannot use an active low GPIO pin, because that pin will go high sometime during the boot process, and that would cut the power while the Pi is booting)

Here is the Watchdog-Timer section with the new Power-off circuit added to the diagram.
Watchdog_Timer_Circuit.png
Watchdog_Timer_Circuit.png (29.78 KiB) Viewed 3678 times
To create the right condition for the power-off pin, you need to add this statement to the dts file we already created for the other GPIO pins.

Code: Select all

[email protected] { function = "output"; termination = "pull_up"; polarity = "active_high"; startup_state = "inactive"; };
I use GPIO-23 but you can use any general purpose pin. I suggest you stay away from the special function ones unless you know their behavior really well.

The “trick” I’m using here is using the feature that the pull states of the GPIO pins will survive a Halt state. They do not survive a power cycle though. During the shutdown process, the GPIO settings will be reset, except for the pull states. So the pull_up termination of the GPIO pin is what gives us the active high level we want. The polarity setting to active_low will “ground” the pin throughout the power on, boot and running process, until the Pi is halted. It will be “reset” at halt, and the pin will go high because of the internal 50K pull-up resister. Simple, now that you know it.

One word of caution : do NOT use gpio.cleanup() because that messes with this feature. This command has not been updated and is currently out of sync with the pin behavior of the Pi.

So by adding the power-off pin setting, the complete pin section will look like this:

Code: Select all

            //
            // setting the GPIO's for the automatic UPS supply
            //
            // using GPIO-23 as the power-down signal pad with an active high output:
            [email protected] { function = "output"; termination = "pull_up"; polarity = "active_high"; startup_state = "inactive"; };
            [email protected]  { function = "input";  termination = "no_pulling"; }; // PWR_Sense
            [email protected] { function = "output"; termination = "pull_up"; polarity = "active_low"; startup_state = "active"; }; // PWR_Gate
            [email protected] { function = "output"; termination = "pull_down"; polarity = "active_high"; startup_state = "active"; }; // PWR_Timer
            //
You compile this with:

Code: Select all

sudo dtc -I dts -O dtb -o /boot/dt-blob.bin my-ups-blob.dts
(there is some more information about the dts blob file a few posts up in this chain)
Reboot to make the changes effective.
And that's really all there is too it.

Enjoy!

PS
Here is the link to my original post to create a Power-off pin using the device tree blob file. viewtopic.php?f=41&t=114975 for more information.

haccks
Posts: 15
Joined: Thu Aug 11, 2016 12:39 pm

Re: HOW-TO Build an Automatic Power Supply for the Pi

Mon Aug 22, 2016 10:51 am

paulv wrote:After having used this supply with my file server for several months, I decided to update/upgrade the firmware to move to Jessie.
Unfortunately, the system stopped working. During the boot, the power was cut by the UPS system.

How could I make it work for Kali Linux? I am not using raspbian.

steveb4pi
Posts: 62
Joined: Sun Aug 11, 2013 6:12 pm

Re: HOW-TO Build an Automatic Power Supply for the Pi

Mon Aug 22, 2016 12:03 pm

Now that we can just use a Power Bank as a UPS the only 'problem' is detecting 'mains loss' and deciding when to shut down the Pi

This (should) be easy eonough :-
1) Detect output from the mains power block (i.e. what's being used to 'recharge' the Power Bank) = to do this all we need is a resistor 'ladder' to drop the 5v to 3v3 so it can be 'sensed' on a Pi GPIO pin (5v to 2k2 to GPIO to 3k3 to GND)
2) Have the 'loss detect' script issue a 'shutdown' command with a time delay ('shutdown -h +50', where +50 means 'in 50 mins') depending on the known capacity of the Power Bank

Many Power Banks come with an 'on off' switch which makes restarting the Pi easy = however if you want the Pi to auto-restart when mains power is restored, that's another issue :-)

haccks
Posts: 15
Joined: Thu Aug 11, 2016 12:39 pm

Re: HOW-TO Build an Automatic Power Supply for the Pi

Wed Sep 28, 2016 3:59 pm

Hi paulv. How much it will cost to make such a power supply? Is there anything like this available in the market that I can buy?

haccks
Posts: 15
Joined: Thu Aug 11, 2016 12:39 pm

Re: HOW-TO Build an Automatic Power Supply for the Pi

Wed Sep 28, 2016 4:04 pm

Hi paulv. How much it will cost to make such a power supply? Is there anything like this available in the market that I can buy?

paulv
Posts: 558
Joined: Tue Jan 15, 2013 12:10 pm
Location: Netherlands

Re: HOW-TO Build an Automatic Power Supply for the Pi

Thu Sep 29, 2016 4:07 am

Hi Haccks,

How much it would cost is difficult to say. If you're a hobbyist, you will have most parts already.
If not, it really depends on where you live and get your parts from.

However, a more modern version is available that can be built relatively easy and not very expensive.
Depending on where you live and where you get your parts.
Here is the link to that post:
viewtopic.php?f=37&t=145954&p
Towards the bottom is a link to a similar supply but with the "automation" of the one you just saw added.

The total price is really depending on the Adafruit controller and the Li-Ion cell.
The rest can be obtained for a few dollars/Euros.

Success!

jaiz.mate
Posts: 3
Joined: Wed May 24, 2017 7:18 am

Re: HOW-TO Build an Automatic Power Supply for the Pi

Wed May 24, 2017 7:33 am

Hi Paul,
Great post!!!
just wondering why can't we use another DC/DC converter instead of two LM317's to give stable current and voltage to trickle charge the battery?

paulv
Posts: 558
Joined: Tue Jan 15, 2013 12:10 pm
Location: Netherlands

Re: HOW-TO Build an Automatic Power Supply for the Pi

Wed May 24, 2017 11:58 am

Thank you!
You can of course use whatever you want, but it's hard to beat the LM317 for price, simplicity and overall real estate as a solution.

jaiz.mate
Posts: 3
Joined: Wed May 24, 2017 7:18 am

Re: HOW-TO Build an Automatic Power Supply for the Pi

Fri May 26, 2017 6:21 am

Hi Paul,

just wondering if we could use IRF9540N instead of Si3443DV. datasheet attached below.
https://www.jaycar.com.au/medias/sys_ma ... etMain.pdf

Si3443DV is not available at my local shop and delivery is taking 6-10 days.

thankyou

Regards
Jas

paulv
Posts: 558
Joined: Tue Jan 15, 2013 12:10 pm
Location: Netherlands

Re: HOW-TO Build an Automatic Power Supply for the Pi

Fri May 26, 2017 10:06 am

Hi jaiz.mate,

No you can't! Have a look at the two datasheets and in particular look at the VGS Threshold specifications and also the RDSon.

If you want to use a replacement, look for a Logic Level MOSFET. They are designed to be driven by a voltage in the 0..5V range.

Success!

jaiz.mate
Posts: 3
Joined: Wed May 24, 2017 7:18 am

Re: HOW-TO Build an Automatic Power Supply for the Pi

Fri May 26, 2017 11:38 am

Thankyou Paul, i will go look for another Mosfet.

Another question, in your version 3.1, In step-up/step-down converter what ground is 12,13 and 10,15 connected to?
Last time i had 12V (adapter) and 5V (Ni-MH) with common ground and zener 5V6 went real hot :oops:
Attachments
sUsD.PNG
sUsD.PNG (5.44 KiB) Viewed 2630 times

paulv
Posts: 558
Joined: Tue Jan 15, 2013 12:10 pm
Location: Netherlands

Re: HOW-TO Build an Automatic Power Supply for the Pi

Fri May 26, 2017 2:55 pm

Hi jaiz.mate,

The ground connections to the convertor are what they are. It depends on what version you use, so don't take the pin numbers for granted. In any case, in these little convertors, the input ground is connected to the output ground. One connection to ground will do. Check with a DMM in the Ohm setting if you are not sure.

If the 5V6 Zener gets hot, it means that the voltage at the Zener is too high (higher than the rated 5.6V), and it starts to try to bleed-off the extra voltage by a "short or connection" to ground. That's why it is getting hot. Check the voltage across the Zener and make sure it is less than 5.6V. I would also check if the Zener is really rated at 5V6, if it is lower, like a common other value is 5V1, that's your problem.

Success!

Return to “Automation, sensing and robotics”