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:
- 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
Step 1. Setup the access point
- Note: use this to set up different repo mirrors, if required: mirror
- Install the access point
- note I did not use any of the ip forwarding stuff.
- install the captive portal
- used qifi to generate the qr code for the website
- Captive portal is working. Edit /etc/nodogsplash/htdocs/splash.html to redirect to the custom webpage using the meta tab
- note we need to use php7.3 which is the default php now
- the instructions for directory permissions were wrong, missing -R on chown of group www-data
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);
}
}