PhotoBooth

My daughter is getting married, and I though it would be fun to build a photobooth. What I wanted what something that took pictures, and saved them locally, so they could be downloaded by participants with phones. No cloud. No internet. Initially I came up with the following:

Photobooth Requirements and Design

Hardware setup

Hardware:

  • Raspberry Pi 4, for speed
  • 8MP rpi camera – should be good enough, need to test.
  • LED strips for “flash”, 12-24 input, probably 12.
  • control circuit required, should be able to use GPIO from the pi to PWM brightness
  • Monitor (hdmi) for preview, instructions, slideshow.
  • DMD display for “countdown” attract mode. etc.
  • Arcade Buttons for picture. Cat 5 connectors for ease of connection, or alternatively, leaning toward using an esp 8266 to control the pictures over WiFi captive portal/url
  • Computer power supply with +5 and +12, high current for DMD and LEDs etc.

Software:

  • Based off of raspian-buster.
  • Picture taker.
  • Overlay picture on green screen.
  • Save Picture with ID.
  • Captive Portal
  • Access code required.
  • Email capture
  • Photo Download.
  • DMD Display Process
  • ESP 8266 button press receiver code.

Implementation, based on pirate box:

I originally thought somthing based off the pirate box might be cool: https://piratebox.cc/raspberry_pi:diy:armbian. This was a fail, raspian image was obsolete, and this didnt provide the required functionality

Implementation, second try:

Next I tried something off of this instructable page. I started off with graphical buster:
then

sudo apt-get update
sudo apt-get upgrade
sudo apt-get dist-upgrade

Install the wireless hotspot

Step 1. Setup the access point

General Software Design

Up to this point we have a camera working with some minimal software

I Used the code from https://github.com/hzeller/rpi-rgb-led-matrix for LED matrix code.

Installed the PCF8523 RTC as per https://learn.adafruit.com/adding-a-real-time-clock-to-raspberry-pi/set-rtc-time

A Usb audio dongle was used (i couldn’t use the builtin display, it conflicted with the DMD.

Final Product

Before I dump you into the code, here are some pictures of the final product:

This is the photobooth in the early development stages

This is the (yuk) python code, I’m not proud of any of this code, but it worked well enough for me:

import glob
import sys
import os
import time
import random
import subprocess
import threading
import shlex
import cv2
import pygame
import picamera
import sounddevice as sd
import soundfile as sf
from PIL import Image
from pygame.locals import *
import RPi.GPIO as GPIO
from http.server import BaseHTTPRequestHandler, HTTPServer

os.environ["SDL_FBDEV"] = "/dev/fb0"

#setup the button webserver
from time import sleep
from http.server import BaseHTTPRequestHandler, HTTPServer

host_name = '192.168.220.1'  # Change this to your Raspberry Pi IP address
host_port = 8000

class Photobooth:
    screen = None
    o = None

################################################################################
#init
    def __init__(self):
        global ScreenSize , backgrounds, awb
        "Ininitializes a new pygame screen using the framebuffer"
        # Based on "Python GUI in Linux frame buffer"
        # http://www.karoltomala.com/blog/?p=679
        disp_no = os.getenv("DISPLAY")
        if disp_no:
            print ("I'm running under X display = {0}" + disp_no)

        # Check which frame buffer drivers are available
        # Start with fbcon since directfb hangs with composite output
        drivers = ['fbcon', 'directfb', 'svgalib']
        found = False
        for driver in drivers:
            # Make sure that SDL_VIDEODRIVER is set
            if not os.getenv('SDL_VIDEODRIVER'):
                os.putenv('SDL_VIDEODRIVER', driver)
            try:
                pygame.display.init()
            except pygame.error:
                print ("Driver: {0} failed." + driver)
                continue
            found = True
            break

        if not found:
            raise Exception('No suitable video driver found!')

        ScreenSize = (pygame.display.Info().current_w, pygame.display.Info().current_h)
        print ("Framebuffer size: %d x %d" % (ScreenSize[0], ScreenSize[1]))
        self.screen = pygame.display.set_mode(ScreenSize, pygame.FULLSCREEN)
        # Clear the screen to start
        self.screen.fill((0, 0, 0))
        # Initialise font support
        pygame.font.init()
        # Render the screen
        pygame.display.update()
        pygame.mouse.set_visible(False)

        #load in the background images
        backgrounds = glob.glob('Backgrounds/*.jpg')
        bgidx = -1
        print("Backgrounds found: ")
        print( *backgrounds)

        
################################################################################
#destrctor
    def __del__(self):
        pygame.quit()
        #Destructor to make sure pygame shuts down, etc.

################################################################################
# slideshow
    # Shows an arbitrary picture from the AllPicturesList
    def slideshow(self):
        global AllPicturesList, PictureFolder, NumberOfPicturesTaken, IsCamActive, RandomPicCount
        if IsCamActive is True:  # Stop preview if it is active.
            self.StopPreview()

        #unload overlay 
        if self.o:
            self.o.close()

        # Load a random Picture from the list.
        RandomPicCount += 1
        if RandomPicCount > 10:
            rannum = 0
            RandomPicCount = 0
        else:
            rannum = random.randint(0, NumberOfPicturesTaken - 1)

        print ("load picture %d" % rannum)
        NextPictureName = AllPicturesList[rannum]
        TempPicture = os.path.join(PictureFolder, NextPictureName)
        TempImage = pygame.image.load(TempPicture).convert()
        TempScreen = pygame.transform.scale(TempImage, (1440, 900))  
        PB.screen.blit(TempScreen, (0, 0))                    

        # Display the image-name
        if rannum > 1 :          # Skip if it is the instruction picture
            font = pygame.font.Font(None, 40)
            PictureNameText = font.render(str(NextPictureName), True, ( 0xFF, 0xFF, 0xFF ))  # White text
            PB.screen.blit(PictureNameText, (50, 700))

        # Update Display
        pygame.display.update()

    # Function to start the preview
    def startPreview(self):
        global cam, IsCamActive, CamFrameRate
        # Clear the screen to start
        PB.screen.fill((0, 0, 0))
        pygame.display.update()
        #cam.awb_mode = 'incandescent'
        cam.awb_mode = 'off'
        cam.awb_gains = (1.1,2.2)
        cam.resolution = PictureSize
        cam.framerate = CamFrameRate
        cam.hflip = True
        cam.start_preview()
        # time.sleep(0)
        IsCamActive = True


    # Function to overlay the preview with the countdown images
    def CD(self,message):
        global LastPictures, cam, IsCamActive
        
        # turn on the cam if required, this will give the users time to set up their positions
        if IsCamActive is False:
            self.startPreview()

        # need to turn the lights off when using matrix
        os.system("sh lightsoff.sh")            
        # give a countdown
        if message is True:
            os.system("../rpi-rgb-led-matrix/examples-api-use/demo --led-cols=64  --led-slowdown-gpio=5 -D 1 ~pi/smile.ppm -m 6 -t 4")
        os.system("sh lightson.sh")
        self.takePicture()
        os.system("sh lightsdim.sh")

    def qroverlay(self):
        img = Image.open("QR.png")
        # // is integer division
        pad = Image.new('RGB', (
             ((img.size[0] + 31) // 32) * 32,
             ((img.size[1] + 15) // 16) * 16,
             ))
        pad.paste(img, (0, 0))
        o = cam.add_overlay(pad.tobytes(), size=img.size, alpha=255, layer=4, fullscreen=False, window=(32, 20, img.size[0], img.size[1]))
        # time.sleep(1)


    def nextbackground(self):
        global backgrounds, bgidx
        # go to live
        self.startPreview()
        bgidx = bgidx +1
        self.displaybackground()
 
    def prevbackground(self):
        global backgrounds, bgidx
        # go to live
        self.startPreview()
        bgidx = bgidx - 1
        self.displaybackground()

    def displaybackground(self):
        global backgrounds, bgidx
        if bgidx < 0:
            bgidx = len(backgrounds) -1
        if bgidx >= len(backgrounds):
            print ("Clearing background")
            bgidx = -1
            self.o.close()
            self.printDmd("No Background")
        else:
            bgstr = backgrounds[bgidx]
            bgstr = bgstr[12:-4]
            print ("using background" + bgstr)
            img = Image.open(backgrounds[bgidx])
            # // is integer division
            pad = Image.new('RGB', (
                ((img.size[0] + 31) // 32) * 32,
                ((img.size[1] + 15) // 16) * 16,
            ))
            pad.paste(img, (0, 0))
            if self.o:
                self.o.close()
            self.o = cam.add_overlay(pad.tobytes(), size=img.size, alpha=128, layer=3, fullscreen=True)
            self.printDmd(bgstr)

    # Function to stop preview
    def StopPreview(self):
        global cam, IsCamActive
        if IsCamActive is True:
            print ("Stop preview")
            cam.stop_preview()
            IsCamActive = False

    def image_manipulation(self, front_file, save_folder):
        img = cv2.imread(front_file)
        width = img.shape[0]  # To select the right background and overlay
        height = img.shape[1]
        print ("width and height" + str(width) + " " +str (height))
        if (bgidx >= 0):
            back = cv2.imread(backgrounds[bgidx])
            width = back.shape[0]  # To select the right background and overlay
            height = back.shape[1]   
            print ("width and height" + str(width) + " " +str (height))
            reds = img[:, :, 2]
            greens = img[:, :, 1]
            blues = img[:, :, 0]
            #orig was 20
#            mask = ((reds < (greens - 20))
#                    & (blues < (greens - 20))
#                   & (greens > 35))
#            mask = ((reds < (greens  ))   # this is all foreground
#                    & (blues < (greens  ))
#                    & (greens > 35))
#            mask = ((reds < (greens  ))   # this is all foreground
#                    & (blues < (greens  ))
#                    & (greens > 35))
#            mask = ((reds < (greens + 10 ))   # this is all foreground
#                    & (blues < (greens + 10 ))
#                    & (greens > 35))
# this is getting there a little too much forground
            mask = ((reds < (greens + 30 )) & (blues < (greens + 30 )) & (greens > 35))
            mask = ((reds < (greens )) & (blues < (greens )) & (greens > 35))
#            mask = ((reds < (greens + 40 ))   # this is getting there a little too much forground
#                    & (blues < (greens + 40 ))
#                    & (greens > 35))
#            mask = (greens > 35)   # this is mostly background
            try:
                img[mask] = back[mask]
            except IndexError:
                logging.error("Background dimensions should be {0}x{1}"
                            .format(width, height))
                sys.exit()
        basename = os.path.basename(front_file)
        basename = os.path.join(save_folder, basename)
        flipped = cv2.flip(img,1)
        cv2.imwrite(basename, flipped)
        return basename

    # Function to take a picture
    def takePicture(self):
        global cam, IsCamActive, Count
        if IsCamActive is False:
            self.startPreview()
        NewPictureName = "img_" + time.strftime("%Y%m%d%H%M%S") + ".png"
        data, fs = sf.read("shutter.wav", dtype='float32')
        #sleep(1)
        cam.capture(NewPictureName)          # Take the picture!
        sd.play(data, fs)
        LastPictures.append(NewPictureName)  # keep it on the list for combining
        self.image_manipulation(NewPictureName, 'Pictures')

    def printDmd(self, str):
        os.system("sh lightsoff.sh")
        os.system("../rpi-rgb-led-matrix/examples-api-use/scrolling-text-example -f ../rpi-rgb-led-matrix/fonts/10x20.bdf --led-slowdown-gpio=5 -s 10 " + str + " --led-cols=64 -l 1")

class MyServer(BaseHTTPRequestHandler):
    def do_HEAD(self):
        self.send_response(200)
        self.send_header('Content-type', 'text/html')
        self.end_headers()

    def do_GET(self):
        html = '''
           <html>
           <body style="width:960px; margin: 20px auto;">
           <h1>Photobooth control</h1>
           <p>Picture: <a href="/pic">take picture</a> <br> <a href="/next">next background</a><br><a href="/prev">previous background</a></p>
           <br><a href="/shutdown">shutdown the system</a></p>
           <br><a href="/reboot">reboot the system</a></p>
           <h2> Last button press </h2>
           <div id="last-but"></div>
           <script>
               document.getElementById("last-but").innerHTML="{}";
           </script>
           </body>
           </html>
        '''
        self.do_HEAD()
        status = ''
        if self.path=='/':
            print ("nothing")
        elif self.path=='/pic':
            e1 = pygame.event.Event(pygame.USEREVENT, attr1='pic')
            pygame.event.post(e1)
            status='take pic'
        elif self.path=='/next':
            e1 = pygame.event.Event(pygame.USEREVENT, attr1='next')
            pygame.event.post(e1)
            status='next background'
        elif self.path=='/prev':
            e1 = pygame.event.Event(pygame.USEREVENT, attr1='prev')
            pygame.event.post(e1)
            status='prev background'
        elif self.path=='/shutdown':
            e1 = pygame.event.Event(pygame.USEREVENT, attr1='shutdown')
            pygame.event.post(e1)
            status='toast'
        elif self.path=='/reboot':
            e1 = pygame.event.Event(pygame.USEREVENT, attr1='reboot')
            pygame.event.post(e1)
            status='try again'
        self.wfile.write(html.format(status).encode("utf-8"))


# Global variables
cam = picamera.PiCamera()   # There is only one camera
IsCamActive = False         # Is the camera-preview active?
LastPictures = []           # List of the last pictures for combining
SlideshowTimerCounter = 0   # counter to trigger the slideshow
AllPicturesList = []        # List of all pictures ever taken
PictureFolder = os.path.join(
    os.getcwd(), 'Pictures')  # folder for the pictures
NumberOfPicturesTaken = 1   # how many pictures are in the AllPicturesList?

PictureSize = (1440, 900)  # Size for the camera CGR make this max!
CamFrameRate = 12           # We don't need 25fps. relax!

#set size of the screen
ScreenSize = 1440, 900

RandomPicCount = 0

backgrounds = None
bgidx = -1


# external shell-script-call: imagemagick convert ...
def combine():
    global LastPictures
    subprocess.call(['/home/pi/pb/combine.sh', LastPictures[0],
                     LastPictures[1], LastPictures[2], LastPictures[3]])
    LastPictures = []

# Update the list of all pictures taken, may be optimised for not to read all files again, but only add the new ones...
def UpdateAllPicturesList():
    global AllPicturesList, NumberOfPicturesTaken, PictureFolder
    AllPicturesList = []
    NumberOfPicturesTaken = 0
    # this will include the splash screen
    for dat in os.listdir(PictureFolder):
        if dat.endswith('.png'):
            AllPicturesList.append(dat)
            NumberOfPicturesTaken += 1

def safeshutdown(arg):
        # shutdown our Raspberry Pi
        os.system("sudo shutdown -h now")

def thread_function(name):
    print("Thread %s: starting"% name)
    http_server = HTTPServer((host_name, host_port), MyServer)
    print("Server Starts - %s:%s" % (host_name, host_port))
    http_server.serve_forever()
    print("Thread %s: finishing"% name)


# Main
print("Start")
PB = Photobooth()
print("Photobooth created")
os.system("sh lightsoff.sh")
data, fs = sf.read("ms.wav")
ZisPic = os.path.join(os.getcwd(), 'zis.png')
TempImage = pygame.image.load(ZisPic).convert()
TempScreen = pygame.transform.scale(TempImage, (1440, 900))
PB.screen.blit(TempScreen, (0, 0))
pygame.display.update()
sd.play(data, fs)
os.system("../rpi-rgb-led-matrix/examples-api-use/demo --led-cols=64  --led-slowdown-gpio=5 -D 1 ~pi/pb/zis.ppm -m 6000 -t 1")
os.system("../rpi-rgb-led-matrix/examples-api-use/scrolling-text-example -f ../rpi-rgb-led-matrix/fonts/10x20.bdf --led-slowdown-gpio=5 -s 5 `date` --led-cols=64 -l 2");
status = sd.wait()
    

PB.startPreview()
print("preview running")

#start button webserver thread
x = threading.Thread(target=thread_function, args=(1,))
print("thread created")
x.daemon = True
x.start()
print("thread running")
time.sleep(0.1)
o = None


# Implement Button-Callback to shut down the raspberry.
#GPIO.add_event_detect(10, GPIO.RISING, callback=safeshutdown, bouncetime=300)

# After start, show the instructions
AllPicturesList.append('Splash.png')
SlideshowTimerCounter = 140  #immediately start with info screen after booting
while True:
    # get key and webserver events 
    event = pygame.event.poll()     
    
    # any key event just shuts down, typically no keyboard
    if event.type == pygame.KEYDOWN:
        pygame.quit()
        sys.exit()
    
    # just print out a user event
    if event.type == USEREVENT:
        #e1 = pygame.event.Event(pygame.USEREVENT, attr1='attr1')
        print (f'Event found {event}')

    # the next event is for next background
    if ( event.type == USEREVENT and event.attr1=='next'):        
        PB.nextbackground()
        # Reset the Slideshow-Counter
        SlideshowTimerCounter = 0
        for event in pygame.event.get():
            # empty the queue
            pass
        
    # the prev event is for the previous background, but just exit the program
    if ( event.type == USEREVENT and event.attr1=='prev'):
        PB.prevbackground()
        # Reset the Slideshow-Counter
        SlideshowTimerCounter = 0
        for event in pygame.event.get():
            # empty the queue
            pass

        # CGR REMOVE THIS
#        pygame.quit()
#        sys.exit()
        
    # a real shutdown event from the webpage
    if ( event.type == USEREVENT and event.attr1=='shutdown'):
        pygame.quit()   
        os.system("shutdown -h now")
        sys.exit()
        
    # a real reboot event from the webpage
    if ( event.type == USEREVENT and event.attr1=='reboot'):      
        pygame.quit()  
        os.system("shutdown -r now")
        sys.exit()
        
    # If there was a BIG-ASS Button-Event (TM): Go and take some pictures
    if event.type == pygame.MOUSEBUTTONDOWN or ( event.type == USEREVENT and event.attr1=='pic'):
        PB.CD(True)  # First picture
        PB.CD(False)  # second picture
        PB.CD(False)  # third picture
        PB.CD(False)  # fourth picture
        os.system("sh lightsoff.sh")            
        # put up wait message
        os.system("../rpi-rgb-led-matrix/examples-api-use/demo --led-cols=64  --led-slowdown-gpio=5 -D 1 ~pi/Wait.ppm -m 6 -t 1")
        combine()   # and combine them into one...
        for event in pygame.event.get():
            # empty the queue
            pass

        # Update all pictures list
        UpdateAllPicturesList()

        os.system("../rpi-rgb-led-matrix/examples-api-use/demo --led-cols=64  --led-slowdown-gpio=5 -D 1 ~pi/Ready.ppm -m 6 -t 1")

        os.system("sh lightsdim.sh")            
        # Overlay the last QR-Code to preview
        PB.qroverlay()

        # Reset the Slideshow-Counter
        SlideshowTimerCounter = 0
    else:
        SlideshowTimerCounter += 1    # increment Timer-Counter
        time.sleep(0.1)
        if SlideshowTimerCounter > 120:
            # Start Slideshow after 12s
            if SlideshowTimerCounter % 40 == 0:
                # Change picture every 4s
                PB.slideshow()
                


The web code is lifted right off of the instructables page:

<!DOCTYPE HTML >

<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">

<title> Photobooth Homepage </title>

<style type="text/css">

body { background-color:#000000; color: white; }

.wrapButtons {
    text-align:center;
    font-size:60px;
    font-family:"Verdana";
    }

</style>

</head>

<body>
<h1> All pictures </h1>
<b>
<?php
$timestamp = time();
$date = date("d.m.Y",$timestamp);
$hour = date("H:i:s",$timestamp);
echo $date," - ",$hour," ";
?>
</b>

<br>
<p>

<?php
$picroot = "./pics";
 
if (is_dir($picroot))
{
    if ( $handle = opendir($picroot) )
    {
        while (($file = readdir($handle)) !== false)
        {
            if ( filetype( $picroot.'/'.$file) == "file"
                 AND substr( $picroot.'/'.$file, -4) == ".jpg")
            {
                $fullpath[] = $picroot.'/'.$file;
            }
        }
        closedir($handle);
    }
}
 
rsort($fullpath);

foreach ( $fullpath AS $dateiname )
{
    echo "<img src=\"$dateiname\"";
    echo " alt=\"";
    echo '" \> ';
}

?>

<!-- this function is created by chuck from http://webcheatsheet.com/php/create_thumbnail_images.php -->

<?php
function createThumbs( $pathToImages, $pathToThumbs, $thumbWidth )
{
  // open the directory
  $dir = opendir( $pathToImages );

  // loop through it, looking for any/all JPG files:
  while (false !== ($fname = readdir( $dir ))) {
    // parse path for the extension
    $info = pathinfo($pathToImages . $fname);
    // continue only if this is a JPEG image
    if ( strtolower($info['extension']) == 'jpg' )
    {
      echo "Creating thumbnail for {$fname} <br />";

      // load image and get image size
      $img = imagecreatefromjpeg( "{$pathToImages}{$fname}" );
      $width = imagesx( $img );
      $height = imagesy( $img );

      // calculate thumbnail size
      $new_width = $thumbWidth;
      $new_height = floor( $height * ( $thumbWidth / $width ) );

      // create a new temporary image
      $tmp_img = imagecreatetruecolor( $new_width, $new_height );

      // copy and resize old image into new image
      imagecopyresized( $tmp_img, $img, 0, 0, 0, 0, $new_width, $new_height, $width, $height );

      // save thumbnail into a file
      imagejpeg( $tmp_img, "{$pathToThumbs}{$fname}" );
    }
  }
  // close the directory
  closedir( $dir );
}
// call createThumb function and pass to it as parameters the path
// to the directory that contains images, the path to the directory
// in which thumbnails will be placed and the thumbnail's width.
// We are assuming that the path will be a relative path working
// both in the filesystem, and through the web for links
createThumbs("pics/","pics/thumbs/",100);
?>

<?php
function createGallery( $pathToImages, $pathToThumbs )
{
  echo "Creating gallery.html <br />";

  $output = "<html>";
  $output .= "<head><title>Thumbnails</title></head>";

  $output .= "<style type=\"text/css\">";
  $output .= "body { background-color:#000000; color: white; }";
  $output .= ".wrapButtons {";
  $output .= "    text-align:center;";
  $output .= "    font-size:60px;";
  $output .= "    font-family:\"Verdana\";";
  $output .= "    }";
  $output .= "</style>";

  $output .= "<body>";
  $output .= "<table cellspacing=\"0\" cellpadding=\"2\" width=\"500\">";
  $output .= "<tr>";

  // open the directory
  $dir = opendir( $pathToThumbs );

  $counter = 0;
  // loop through the directory
  while (false !== ($fname = readdir($dir)))
  {
    // strip the . and .. entries out
    if ($fname != '.' && $fname != '..')
    {
      $output .= "<td valign=\"middle\" align=\"center\"><a href=\"{$pathToImages}{$fname}\">";
      $output .= "<img src=\"{$pathToThumbs}{$fname}\" border=\"0\" />";
      $output .= "</a></td>";

      $counter += 1;
      if ( $counter % 4 == 0 ) { $output .= "</tr><tr>"; }
    }
  }
  // close the directory
  closedir( $dir );

  $output .= "</tr>";
  $output .= "</table>";
  $output .= "</body>";
  $output .= "</html>";

  // open the file
  $fhandle = fopen( "gallery.html", "w" );
  // write the contents of the $output variable to the file
  fwrite( $fhandle, $output );
  // close the file
  fclose( $fhandle );
}
// call createGallery function and pass to it as parameters the path
// to the directory that contains images and the path to the directory
// in which thumbnails will be placed. We are assuming that
// the path will be a relative path working
// both in the filesystem, and through the web for links
createGallery("pics/","pics/thumbs/");
?>
<p>

</body>
</html>

The most fun part of the code was building the remote control button, the ESP8266 code is here:

/**********************************************
 * Edit Settings.h for motor config etc (motors? there are no motors
 * in this project, or settings.h :)
 ***********************************************/
#include "ChangeMac.hpp"
#include <ESP8266HTTPClient.h>
#include <ESP8266HTTPUpdateServer.h>
#include <ESP8266WebServer.h>
#include <ESP8266WiFi.h>
#include <ESP8266mDNS.h>
#include <WiFiManager.h>

const char* ssid     = "bill";
const char* password = "gertrude"; /* password must be 8 char or more for WPA */

#define VERSION "0.999"

#define HOSTNAME "PhotoButton-"
#define CONFIG "/conf.txt"

/* Useful Constants */
#define SECS_PER_MIN (60UL)
#define SECS_PER_HOUR (3600UL)
#define SECS_PER_DAY (SECS_PER_HOUR * 24L)

/* Useful Macros for getting elapsed time */
#define numberOfSeconds(_time_) (_time_ % SECS_PER_MIN)
#define numberOfMinutes(_time_) ((_time_ / SECS_PER_MIN) % SECS_PER_MIN)
#define numberOfHours(_time_) ((_time_ % SECS_PER_DAY) / SECS_PER_HOUR)
#define elapsedDays(_time_) (_time_ / SECS_PER_DAY)

// Time
long   lastEpoch        = 0;
long   firstEpoch       = 0;
long   displayOffEpoch  = 0;
String lastMinute       = "xx";
String lastSecond       = "xx";
String lastReportStatus = "";

// declairing prototypes
void configModeCallback(WiFiManager* myWiFiManager);

int8_t getWifiQuality()
{
    int32_t dbm = WiFi.RSSI();
    if (dbm <= -100) {
        return 0;
    } else if (dbm >= -50) {
        return 100;
    } else {
        return 2 * (dbm + 100);
    }
}
// GPIOS for button/lights
int BUTTON_LIGHT = 2;
int PIC_BUTT     = 0;
int NEXT_BUTT    = 12;
int PREV_BUTT    = 14;

// initial stack
char* stack_start;

void
printStackSize()
{
    char stack;
    Serial.print(F("stack size "));
    Serial.println(stack_start - &stack);
}

bool
captiveLogin()
{
    static const char* LOCATION       = "Location";
    static const char* AUTH_TARGET    = "authtarget: ";
    static const char* HEADER_NAMES[] = {LOCATION};

    String uri;
    {
        HTTPClient http;
        http.begin("http://192.168.220.1/index.html");
        http.collectHeaders(HEADER_NAMES, 2);
        int httpCode = http.GET();
        if (httpCode == 200) { return true; }
        if (httpCode != 307 || !http.hasHeader(LOCATION)) { return false; }
        uri = http.header(LOCATION);
        Serial.print("portal=");
        Serial.println(uri);
        delay(2000);
    }

    String auth_target_str;
    {
        HTTPClient http;
        http.begin(uri);
        http.collectHeaders(HEADER_NAMES, 2);
        int httpCode = http.GET();
        if (httpCode != 200) { return false; }
        Serial.println("Got portal page.  Now looking for the auth token");
        String payload = http.getString();
        // Serial.println(payload);
        size_t tok_loc = payload.indexOf("authtarget: ");
        auth_target_str =
            payload.substring(tok_loc, tok_loc + 79); /* beware magic number */
        auth_target_str.concat("http://192.168.220.1/index.html");
        auth_target_str.remove(0, 12);
        Serial.print("cookie=");
        Serial.println(auth_target_str);
        delay(1000);

        Serial.print("Sending authentication");
        http.begin(auth_target_str);
        httpCode = http.GET();
        Serial.print("got HTTP return code ");
        Serial.println(httpCode);
        if (httpCode != 200) { return false; }
        payload = http.getString();
        Serial.println(payload);
        delay(1000);

        Serial.println("Get first simple page");
        http.begin("http://192.168.220.1/index.html");
        httpCode = http.GET();
        Serial.print("got HTTP return code ");
        Serial.println(httpCode);
        if (httpCode != 200) { return false; }
        payload = http.getString();
        Serial.println(payload);
        delay(1000);
    }
    {
        Serial.println("Get first simple page, again");
        HTTPClient http;
        http.begin("http://192.168.220.1/index.html");
        int httpCode = http.GET();
        Serial.print("got HTTP return code ");
        Serial.println(httpCode);
        if (httpCode != 200) { return false; }
        String payload = http.getString();
        Serial.println(payload);
        delay(1000);
    }
}

void
flash_led(int number, int dly)
{
    while (number--) {
        digitalWrite(BUTTON_LIGHT, LOW);
        delay(dly);
        digitalWrite(BUTTON_LIGHT, HIGH);
        delay(dly);
    }
}


int
any_butt()
{
    return (!digitalRead(PIC_BUTT) || !digitalRead(PREV_BUTT) ||
            !digitalRead(NEXT_BUTT));
}

void
breathe_led()
{
    for (int i = 0; i < 1000; i++) {
        analogWrite(BUTTON_LIGHT, i);
        if (any_butt()) { return; }
        delay(1);
    }
    for (int i = 0; i < 1000; i++) {
        analogWrite(BUTTON_LIGHT, 1000 - i);
        if (any_butt()) { return; }
        delay(1);
    }
}

void
setup()
{
    /* this is start of the photobutton connection to the captive portal */
    Serial.begin(115200);
    Serial.println();
    Serial.println();

    Serial.println("Flash light for a bit");
    pinMode(BUTTON_LIGHT, OUTPUT);
    flash_led(10, 50);
    breathe_led();

    WiFi.mode(WIFI_STA);
    WiFi.persistent(false);

    uint8_t mac[6];
    makeRandomMac(mac);
    changeMac(mac);
    Serial.print("MAC address is ");
    Serial.println(WiFi.macAddress());

    String hostname = "PBButton-";
    hostname += random(10);
    hostname += random(10);
    hostname += random(10);
    hostname += random(10);
    WiFi.hostname(hostname);
    Serial.print("Hostname is ");
    Serial.println(hostname);

    Serial.print("Connecting to ");
    Serial.println(ssid);
    WiFi.begin(ssid, password);
    while (WiFi.status() != WL_CONNECTED) {
        delay(500);
        Serial.print(WiFi.status());
    }

    Serial.println("");
    Serial.println("WiFi connected");
    Serial.println("IP address: ");
    Serial.println(WiFi.localIP());

    if (!captiveLogin()) { ESP.restart(); }

    flash_led(30, 20);

    // init record of stack
    char stack;
    stack_start = &stack;

    // print the received signal strength:
    Serial.print("Signal Strength (RSSI): ");
    Serial.print(getWifiQuality());
    Serial.println("%");

    flash_led(5, 100);

    Serial.println("*** Leaving setup()");

    pinMode(PIC_BUTT, INPUT_PULLUP);
    pinMode(PREV_BUTT, INPUT_PULLUP);
    pinMode(NEXT_BUTT, INPUT_PULLUP);
}

//************************************************************
// Main Looop
//************************************************************
void
loop()
{

    breathe_led();
    printf("breathe\n");
    if (!digitalRead(PIC_BUTT)) {
        HTTPClient http;
        http.begin("http://192.168.220.1:8000/pic");
        int httpCode = http.GET();
        Serial.print("got HTTP return code ");
        Serial.println(httpCode);
        delay(20000);
    }
    if (!digitalRead(PREV_BUTT)) {
        HTTPClient http;
        http.begin("http://192.168.220.1:8000/prev");
        int httpCode = http.GET();
        Serial.print("got HTTP return code ");
        Serial.println(httpCode);
        delay(500);
    }
    if (!digitalRead(NEXT_BUTT)) {
        HTTPClient http;
        http.begin("http://192.168.220.1:8000/next");
        int httpCode = http.GET();
        Serial.print("got HTTP return code ");
        Serial.println(httpCode);
        delay(500);
    }
}