Raspberry Pi Setup and Reference

Raspberry Pi Zero and Camera

Based around a description of making a wi-fi IP camera using a Raspberry Pi, this is a one-stop reference for many essential Raspberry Pi setup and installation procedures.

Included here are:

Raspberry Pi initial setup

Turning a Raspberry Pi into a camera

Adding extra features

Although the project cost of an IP camera based on a Raspberry Pi can be similar to a low-end commercial IP camera once the expenditure on all the required components is totalled, a Raspberry Pi-based camera is very flexible and can fairly easily be adapted to suit changing requirements.

Raspberry Pi Zero camera in an audio cassette case
An example camera using a Raspberry Pi Zero and separate wi-fi dongle (without UPS and temperature sensor) housed in an audio cassette case.

In addition to its use as an IP camera, this design can be used for other purposes by simply changing a few settings. For example:

The software will also work without a camera for remote trigger sensing and temperature measurement. The system can be remotely controlled, even if behind a firewall or on a private network.

Any version of the Raspberry Pi can be used, depending on requirements. Each Raspberry Pi version offers something different - for example a Pi 3 has built-in wi-fi and ethernet connections and a Pi Zero is very small and cheap but, unlike the Pi Zero W, needs a separate wi-fi dongle. The Pi Zero and Pi Zero W need a special cable for the Raspberry Pi Camera Board but this is included with the official Pi Zero case.

Raspberry Pi camera system diagram

Operating system installation

The first step in installing the software is to configure the SDHC card and enable wi-fi so that the Raspberry Pi can be easily updated and configured from a PC. There are at least three ways to do this which are well documented elsewhere but are outlined here for convenience:

  1. SDHC card with NOOBS files copied to it - requires that the HDMI connection works on an available tv and a USB keyboard is available. A powered USB hub may also be required.
  2. SDHC card with a Raspbian image flashed to it - does not require a monitor, keyboard or hub. However, note that some advertised 'Raspbian' cards are actually NOOBS cards and need to be installed as option 1.
  3. SDHC card set up on another Raspberry Pi first.

SSH will be activated on the Raspberry Pi and allows the RPi terminal screen to be seen on a PC (the latest versions of the OS have this disabled by default). A suitable SSH program will need to be installed on a PC so Google for a reliable source of putty.exe and download and install it on the PC. In the descriptions below, if entering the appropriate name into to the Host Name box doesn't work try entering the Raspberry Pi's IP address, which may be found by logging in to the local network's router.

Initial setup option 1 - using NOOBS with a tv and keyboard

Either obtain a ready-prepared NOOBS SDHC card or format the SDHC card and download and copy the NOOBS files to it (details here). Then:

The installation can be restarted if required by holding down the 'Shift' key when rebooting.

Login as 'pi' with password 'raspberry'. If the graphical desktop appears, hold down Ctrl and press Esc then arrow down to 'Accessories' then right to 'Terminal' or use Ctrl-Alt-T to directly open a terminal window.

Enable SSH by typing:

sudo raspi-config

then press Enter and select Option 5 ('Interfacing options') and then the 'Enable SSH' option. Tab to 'Finish' and press Enter to reboot.

Once the 'Enable wi-fi' section below has been completed the USB keyboard can be replaced with a wi-fi dongle (or the internal wifi interface can be used if installed). Even if an ethernet connection is to be used it's worth having the flexibility of a wi-fi connection. After rebooting, the subsequent actions take place on the PC's Putty terminal screen.

It may be helpful to prevent the RPi screensaver blanking the screen after 30 minutes (although video from the camera will always be displayed when it's active). To disable the screensaver type

sudo nano /etc/kbd/config

and press Enter then change




Hold down the Ctrl key and press X, then Y, then Enter to save the changes (this change takes effect after the next reboot).

However, if the video/HDMI output from the camera is to be used in a practical application then the BLANK_TIME can be set to 1 so that the console display is removed as a distraction in the camera video borders as soon as possible after starting

This setting doesn't affect the console display shown on an SSH-connected computer.

Initial setup option 2 - using a disk image on a PC

Download the full Raspbian image (details here) onto a PC and use Win32DiskImager (run as administrator) or similar to transfer to the SDHC card. Alternatively, obtain a ready-prepared SDHC card containing an installed Raspbian image.

The remainder of a Raspberry Pi Zero installation can be made easier by making it function as a USB device. Edit the SDHC card contents while still on the PC:

  • Edit config.txt to add dtoverlay=dwc2 (if not already present).
  • Edit cmdline.txt to add modules-load=dwc2,g_ether after rootwait (if not already present).
  • Create an empty file called ssh (with no extension) in the SDHC boot folder - this activates SSH.
  • Transfer the SDHC card from the PC to the Raspberry Pi Zero and plug the Raspberry Pi Zero's USB data port (not the USB power port) into a PC USB port. No power lead is needed.
  • Wait about one minute for the new device to be recognised by the PC.
  • Check that the Raspberry Pi Zero can be accessed via Putty as raspberrypi.local.
  • On the PC's Putty terminal screen for raspberrypi.local (login as 'pi' with password 'raspberry'), complete the 'Enable wi-fi' section below.
  • Remove the Raspberry Pi Zero from the PC and connect a 5-volt power supply (and its wi-fi dongle if not a 'W'). Subsequent actions take place on the PC's Putty terminal screen for raspberrypi (not raspberrypi.local).

If the Raspberry Pi Zero cannot be accessed check that Bonjour is installed and running on the PC.

If the Raspberry Pi Zero still cannot be accessed via Putty as raspberrypi.local, complete the following additional steps (some versions of Windows may have slightly different commands):

  • In Windows Device Manager, find RNDIS/Ethernet Gadget in Other devices, right click and select Update Driver Software.
  • Choose Browse my computer for driver software then Let me pick from a list of device drivers on my computer. Then choose Network adapters and click Next.
  • Select Microsoft Corporation and Remote NDIS based Internet Sharing Device.
  • Click Next and ignore any warnings.

Note that this setup option is designed to work on a Raspberry Pi Zero and will not work on earlier Raspberry Pi versions.

For more information see https://www.raspberrypi.org/blog/programming-pi-zero-usb.

Initial setup option 3 - using another Raspberry Pi

Some Raspberry Pi versions have network and additional USB connections which may make initial setup easier than with the Zero version. Once the wi-fi dongle settings have been completed (see below) the SDHC card can be transferred to the required Raspberry Pi and the remainder of the setup completed.

Enable wi-fi

There are several ways to set up wi-fi on a Raspberry Pi. The method described here ensures that the wi-fi connection will be restored if interrupted. Edit the wpa_supplicant.conf file by typing:

sudo nano /etc/wpa_supplicant/wpa_supplicant.conf

press Enter and add the lines shown highlighted. Use your own router ssid and psk (password) entries.

ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev

The network={ } section can be repeated if there are several known wi-fi networks that the Raspberry Pi will be used with - it will ignore any unavailable networks.

Hold down the Ctrl key and press X, then Y, then Enter to save the changes. Restart the system by entering:

sudo shutdown -r now

and press Enter.



to check the wifi connection - a recognisable ip address should appear in the wlan0 section. If a wi-fi connection cannot be established then check that the wi-fi settings have been correctly entered.

Update the operating system and firmware

With the Raspberry Pi's wi-fi or ethernet connection established (or with a Raspberry Pi Zero successfully connected via a PC's USB port) an internet connection becomes available, so the latest software can be downloaded. Type

sudo apt-get update

and press Enter. When the prompt reappears type

sudo apt-get upgrade

and press Enter. This may take some time to complete and there may be a need for keyboard input along the way.

Change the system name

This will avoid clashes with any other Raspberry Pis on the same network. Type the following command to open the hosts file:

sudo nano /etc/hosts

In the last entry for replace raspberrypi with the new name. Hold down the Ctrl key and press X, then Y, then Enter to save the changes.

Then type the following command to open the hostname file:

sudo nano /etc/hostname

Replace raspberrypi with the new name and hold down the Ctrl key and press X, then Y, then Enter to save the changes. Enter the following command to make the change take effect:

sudo /etc/init.d/hostname.sh

and press Enter. Then type:

sudo reboot

and press Enter.

Where 'raspberrypi' appears as the system name in the following text, replace it with the new system name.

Change the default password

The default password is 'raspberry', which everyone will know so is insecure. Change the password to something unique by typing (without 'sudo'):


and press Enter then follow the instructions on the screen to set the new password. Make a note somewhere of the new password.

If the password is forgotten, either reinstall everything from scratch on a new SD card or remove the SD card and use a PC to temporarily add init=/bin/sh to the end of the line of text in cmdline.txt. With the SD card back in the RPi type

passwd pi

and press Enter. Type the required password and Enter twice then type

sync exec /sbin/init

and press Enter. When the RPi has booted, close it down and use a PC to remove the temporary addition to cmdline.txt.

Set the timezone

Although the correct time might be displayed, setting this option is sometimes necessary to automatically adjust for summer/daylight saving time. Type:
sudo raspi-config

then press Enter and select Option 4 ('Localization options') then the 'Timezone' option and set appropriately. Tab to 'Finish' and press Enter to reboot.

Automatically reboot the Raspberry Pi every night

If an attempt to remotely reboot a camera fails, a fallback is to have the camera reboot itself in the early hours of the next morning - this is particularly useful if the installation is many miles away. Add a line to the cron table to reboot at 4 a.m. each day by typing

sudo crontab -e

and press Enter. Copy this line and paste it at the end of the existing text:

0 4 * * * sudo shutdown -r now

Hold down the Ctrl key and press X, then Y, then Enter to save the changes.

(Note that if using the UPS option, the line should be changed to:

0 4 * * * sudo shutdown now

because the UPS will take care of the reboot.)

Enable the camera

This is most easily done by typing on the PC's Putty terminal screen:
sudo raspi-config

then press Enter and select Option 5 ('Interfacing options') then the 'Enable Camera' option. Tab to 'Finish' and press Enter to reboot.

Create a RAM drive for the camera image and other files

This will reduce wear on the SDHC card. The camera image is saved several times a second which could quickly cause an over-used SDHC card to become read only. First type the following:

sudo mkdir /home/pi/live

and press Enter. Then type:

sudo nano /etc/fstab

and press Enter. Add the following line:

tmpfs /home/pi/live tmpfs nodev,nosuid,size=1M 0 0

Hold down the Ctrl key and press X, then Y, then Enter to save the changes then type:

sudo mount -a

and press Enter.

Install camaction.py, the camera software

This program is needed to monitor and control the camera, respond to events, provide a webserver and ensure that the Raspberry Pi's SDHC card doesn't run out of space. It will work without a camera to detect non-visual triggers but with a camera (which it automatically detects) it will deliver a stream of live jpg images to its built-in web page.

After a script is uploaded to a Raspberry Pi from a PC it may need to be loaded into Nano and resaved to ensure that it is not in DOS format (i.e. has CR+LF line endings). Alt-D toggles DOS format in Nano so the key sequence to fix an open document is Ctrl-O, Alt-D, Y, Ctrl-X.

A couple of libraries need to be installed first:

1 - The Python Imaging Library is used to measure the image. Install PIL by typing:

sudo apt-get install python-imaging

and press Enter.

2 - Paramiko is used for optional SFTP uploads of images to a remote server. Install Paramiko by typing:

sudo apt-get install python-paramiko

and press Enter.

3 - Requests is used to check an optional remote control file. Install requests by typing:

sudo apt-get install python-requests

and press Enter.

Now create the main camaction.py program by typing:

sudo nano ./camaction.py

and press Enter. Copy the following and paste it by clicking the right mouse key (it may take some time to fully paste in):



# Designed for Pi Zero but will work with any Raspberry Pi.
# Automatically detects camera and temperature sensor installation.
# Works in 'text only' if there's no camera.
# Responds to movement, low light, high temperature, low temperature and external trigger.
# Can create events at specific times each day and at regular intervals.
# Uploads images and emails thumbnails.
# Has built-in webserver which automatically finds the next free port.
# Can be remotely controlled.
# Optional automatic reboot if communicatins are lost.
# Camera can work continuously or frame-by-frame to save power.
# Requires /home/pi/live/ directory to have been created on the pi (ideally in ram).
# Requires camdisplay.html and camdisplay.js for web page.
# Optionally can use /home/pi/mask.jpg (any size - white = show, black = hide).
# Uploading to a remote server requires 'camimages' and 'live' directories to have been created on the remote server.

# --------------------- INITIAL SYSTEM SETTINGS ---------------------
# ----- These settings can be overridden by a settings.txt file -----
# --------------- See setup web page for explanations ---------------

width = 820
height = 616
rotation = 0
lowlight = 30000000
use_video = True
frame_interval = 0
mode = 'auto'
contrast = 0
awb = 'auto'
show_label = True
show_label_back = False
label_back = '#80C080'
show_size = False
preview_mode = True

threshold = 5000000
lowlight_threshold = 60000
dark = 0
move_type = 'inc'
event_wait = 4
sequence_qty = 1
sequence_wait = 0

timed_times = []

regular_interval = 0

gpio_pin = 23
trigger_interval = 1

maxsize = 100000000

device_directory = '/sys/bus/w1/devices/'
temp_adj = 0
temp_interval = 30
min_temp = 10
max_temp = 25

port = 80
password = 'pwd'
expiry = 60*60*4
blacklist = []
whitelist = []
maxusers = 10
latest_qty = 150
thumbqty = 8
startup = 'live'

email_actions = []           
email_interval = 20
smtp_server = ''
smtp_user = ''
smtp_pass = ''
encrypted = True
addr_from = ''
addr_to = []
thumb_width = 480
thumb_height = 360

ftp_server = ''
ftp_user = ''
ftp_pass = ''
ftp_directory = ''
use_sftp = True
upload_interval = 10
ftp_url = ''

upload_actions = []
settings_image = True
live_interval = 60
log_interval = 60

sys_name = 'RPi Camera'
poll_interval = 60*60
poll_url = ''
poll_limit = 3
reboot_times = []
version = 'Version 22 (November 2017)'

# ----------- (End of initial system settings) -------------

import threading    
import io    
import os
import sys    
import subprocess     
import time
from datetime import datetime
import picamera  
from PIL import Image    
from PIL import ImageStat       
import math   
from fractions import Fraction    
import RPi.GPIO as GPIO
import smtplib
from smtplib import socket
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.mime.image import MIMEImage
import cStringIO
from StringIO import StringIO
import socket
import pickle
import shutil
import SimpleHTTPServer
import SocketServer
import BaseHTTPServer
import cgi
if use_sftp:
    import paramiko
    import ftplib
import requests

# Ensure that the correct directory is used    
# Load optional local settings file    
if os.path.exists('settings.txt'):         

# Show messages on screen and in the log   
def show_text(msg):
    msg = time.strftime('%Y-%m-%d %H%M:%S') + ' ' + msg
    sys.stdout.write(msg + '\n')

# Convert numbers to bytes
def show_bytes(size):
    for suffix in ['B','KB','MB','GB']:
        if size < 1024:
            if suffix == 'B' or suffix == 'KB':
                return '%s%s' % (int(size), suffix)
                return '%3.1f%s' % (size, suffix)
        size /= 1024.0

# Initialise global variables    
camsupported = False    
camdetected = False
system_active = True
deg = ''
detail = ''
event_log = []
live_directory = '/home/pi/live/'
img_directory = '/home/pi/camimages/'
fieldcount = 100

# Check for existence of live directory
if not os.path.exists(live_directory):
    show_text('Please create the directory ' + live_directory + ', ideally in ram.')

# Create image directory if it doesn't already exist
if not os.path.exists(img_directory):
    show_text(img_directory + ' created.')
# Create pending files if they don't already exist    
if not os.path.isfile(live_directory + 'uploads.pickle'):
    with open(live_directory + 'uploads.pickle', 'wb+') as f:
        data = list()
        pickle.dump(data, f)
if not os.path.isfile(live_directory + 'emails.pickle'):
    with open(live_directory + 'emails.pickle', 'wb+') as f:
        data = list()
        pickle.dump(data, f)
# Create or load image mask
if not os.path.exists('/home/pi/mask.jpg'):
    image = Image.new("RGB", (width, height), (255,255,255))
mask = Image.open('/home/pi/mask.jpg').convert('1')
mask = mask.resize((width,height))        
# Create 'live' file names  
live_temp = live_directory + 'temp.jpg'
live_temp2 = live_directory + 'temp2.jpg'
live_image = live_directory + 'live.jpg'

# Enable GPIO pin for GPIO trigger input
GPIO.setup(gpio_pin, GPIO.IN, pull_up_down = GPIO.PUD_UP) 

def pickleitem(picklelist, item):
    # Adds item to file named picklelist
    with open(live_directory + picklelist, "rb") as f:
            data = pickle.load(f)
        except EOFError:
            data = list()
        if item not in data:
    with open(live_directory + picklelist, "wb") as f:    
        pickle.dump(data, f)

def get_input(field, preset, help):
    global fieldcount
    value = eval(field)
    code = '<p>%s:' % field
    if preset != '':
        presets = preset.split(';')
        code += ' <select name="upd%s_%s">\n' % (fieldcount, field)
        for item in presets:
            code += '<option value="%s"' % item
            if str(item) == str(value):
                code += ' selected'
            code += '>%s</option>' % item
        code += '</select>'
        code += ' <input type="text" name="upd%s_%s" value="%s" size="%s">' % (fieldcount, field, value, len(str(value)))
    code += ' <span class="help">%s</span>' % help
    code += '</p>\n'
    fieldcount += 1
    return code
def call_alarm(alarm, event_time, details):
    # Creates alarm image and adds it to the email and/or uploads queues
    if alarm != '':
        show_text('ALARM: ' + alarm + ' ' + details)
        if camdetected and os.path.exists(live_image): # Save the current image if there is one
            # Create a snapshot image
            image_name = event_time + '_' + alarm + '.jpg'
            image_name = image_name.replace(' ', '_')
            image_name = image_name.replace(':', '-')
            shutil.copyfile(live_image, img_directory + image_name)
            show_text(' - Saved ' + image_name + ' (' + show_bytes(os.path.getsize(img_directory + image_name)) + ')')
            # Cleanup - remove oldest files to reduce disk use back to a specified maximum
            totalsize = 0
            for filename in sorted(os.listdir(img_directory), reverse = True):
                if (os.path.isfile(img_directory + filename)):
                    this_size = os.path.getsize(img_directory + filename)
                    if (totalsize + this_size < maxsize):
                        totalsize += this_size
                        os.remove(img_directory + filename)
                        show_text(' - Deleted ' + filename)
            show_text(' - Space remaining ' + show_bytes(maxsize - totalsize) + ' of ' + show_bytes(maxsize))

            # Add to the emails queue
            if alarm in email_actions:
                pickleitem("emails.pickle", image_name)
                show_text(' - Ready to email ' + image_name)
            # Add to the uploads queue
            if alarm in upload_actions:
                pickleitem("uploads.pickle", image_name)
                show_text(' - Ready to upload ' + image_name)
        elif alarm in email_actions:
            # Send a text-only email
            pickleitem("emails.pickle", alarm)
            show_text(' - Ready to email ' + alarm + ' alarm')
def controller():
    # Checks remote control file and updates settings, reboots if required
    global poll_interval
    global poll_limit
    global live_image

    prev_poll = time.time() - poll_interval + 30 # wait 30s for the network to be available before first check
    poll_counter = poll_limit
    prev_response = ''
        while system_active:
            if poll_interval > 0 and time.time() - prev_poll > poll_interval:
                prev_poll = time.time()
                show_text('Checking control file at ' + poll_url)
                check_result = 'error'
                    response = requests.get(poll_url, timeout = 2)
                    if response.status_code == requests.codes.ok:
                        content = response.text
                        if content.find("sys_name = '" + sys_name + "'") > -1: # valid control file found
                            if response.headers['last-modified'] != prev_response: # control file has changed so read it
                                prev_response = response.headers['last-modified']
                                exec(content, globals())
                                show_text('Control file validated and settings have been updated')
                                check_result = 'updated'

                                if settings_image and os.path.exists(live_image): # create an image when the settings have been updated
                                    pickleitem("uploads.pickle", 'live.jpg')
                                    show_text('live.jpg ready to upload')
                                    check_result = 'live_image_ready'

                                show_text('Settings are up to date')
                                check_result = 'up_to_date'
                            show_text('Control file found but is not valid')
                            check_result = 'not_valid'
                        show_text('Control file not found (%s)' % response.status_code)
                        check_result = 'not_found'
                    # Call the url again to enable a logger to show the response on the end of the url
                    response = requests.get(poll_url + '?result=' + check_result, timeout = 2)
                    poll_counter = poll_limit

                except requests.exceptions.Timeout:
                    show_text('Control file request timed out')
                    poll_counter -=1
                except socket.timeout:
                    show_text('Control file socket timed out')
                    poll_counter -=1
                except requests.exceptions.RequestException as e:
                    show_text('Control file error: %s' % e)
                    poll_counter -=1
            if time.strftime('%H%M%S') in reboot_times or (poll_limit > 0 and poll_counter <= 0):
        show_text(' - Controller finished.')
def imager():
    # Continually updates live.jpg and detects movement
    global camdetected
    global curr
    # Initialise variables
    prev_event = time.time()
    prev = 0
    prev_low = 0
    low_light = False
    curr = lowlight +1
    sequence_counter = 0
    frame_counter = 0
    frame_rate = 0
    prev_frame = time.time()
    prev_frame_rate = 0
    motion_stream = io.BytesIO()
    # Check whether a camera is installed         
    camcheck = subprocess.check_output(['vcgencmd','get_camera'])    
    if camcheck.find('supported=1') > -1:    
        camsupported = True    
        show_text('* This Raspberry Pi supports camera operation.') 
        show_text('* This Raspberry Pi is not configured to use a camera.')   
    if camcheck.find('detected=1') > -1:    
        camdetected = True
        show_text('* Camera detected (%s x %s).' % (width, height))
        show_text('* Camera not detected.')
    if camdetected:
            if frame_interval == 0: #leave the camera running to get the maximum frame rate
                # Initialise the camera
                camera = picamera.PiCamera()
            while system_active:
                frame_counter += 1
                event_time = time.strftime('%Y-%m-%d %H%M:%S')
                if frame_interval >0:
                    # Initialise the camera
                    show_text('Camera on')
                    camera = picamera.PiCamera()
                camera.exposure_mode = mode
                camera.resolution = (width, height)
                camera.rotation = rotation    
                camera.contrast = contrast
                camera.awb_mode = awb
                if show_label_back:
                    camera.annotate_background = picamera.Color(label_back)
                    camera.annotate_background = None
                if preview_mode:
                if show_size:
                    camera.annotate_text = '%s' %int(curr)
                elif show_label:
                    camera.annotate_text = sys_name + '   ' + event_time + '   ' + deg
                    camera.annotate_text = None
                # Capture the image and measure the image size
                camera.capture_sequence([motion_stream], format = 'jpeg', use_video_port = use_video)    
                img = Image.open(motion_stream)
                curr = sum(ImageStat.Stat(img).sum)
                # Low light detection
                if curr < lowlight and not low_light:
                    call_alarm('low-light', event_time, '%s' %int(curr))
                    low_light = True
                if curr >= lowlight:
                    low_light = False
                if lowlight > 0 and low_light:
                    show_text('Preparing for a long exposure image - please wait.')
                    # Save the current camera settings
                    temp_framerate = camera.framerate
                    temp_shutter_speed = camera.shutter_speed
                    temp_exposure_mode = camera.exposure_mode
                    temp_iso = camera.iso
                    temp_contrast = camera.contrast
                    # Set the camera to long-exposure mode
                    camera.contrast = 0
                    camera.framerate = Fraction(1, 6) # Set to minimum frame rate (1 in 6s)
                    camera.shutter_speed = 6000000    # Set to 6-second exposure time
                    camera.iso = 800                  # Set sensitivity to maximum
                    camera.exposure_mode = 'off'
                    # Wait for the camera to settle
                    # Update the timestamp, capture the image and re-measure the image size
                    event_time = time.strftime('%Y-%m-%d %H%M:%S')
                    show_text('Creating long exposure image - please wait.')
                    if show_label:
                        camera.annotate_text += ' *'
                    camera.capture_sequence([motion_stream], format = 'jpeg', use_video_port = False)    
                    img = Image.open(motion_stream)
                    curr_low = sum(ImageStat.Stat(img).sum)
                    os.rename(live_temp, live_image) # Prevents web page read coinciding with image write
                    # Restore the camera settings
                    camera.framerate = temp_framerate
                    camera.shutter_speed = temp_shutter_speed
                    camera.exposure_mode = temp_exposure_mode
                    camera.iso = temp_iso
                    camera.contrast = temp_contrast
                    show_text('Long exposure image ready.')
                    # Lowlight movement detection
                    diff = 0
                    if move_type == 'inc':
                        diff = curr_low - prev_low
                    elif move_type == 'dec':
                        diff = prev_low - curr_low
                    else: # both inc and dec cause event
                        diff = math.fabs(curr_low - prev_low)
                    if diff < 0:
                        diff = 0
                    if prev_low > 0 and lowlight_threshold > 0 and diff > lowlight_threshold:
                        call_alarm('movement', event_time, '%s-%s=%s' %(int(curr_low), int(prev_low), int(diff)))
                    prev_low = curr_low
                    os.rename(live_temp, live_image) # Prevents web page read coinciding with image write   
                    # Normal movement detection
                    diff = 0
                    if move_type == 'inc':
                        diff = curr - prev
                    elif move_type == 'dec':
                        diff = prev - curr
                    else: # both inc and dec cause event
                        diff = math.fabs(curr - prev)
                    if diff < 0:
                        diff = 0
                    if prev > 0 and curr > dark and threshold > 0:
                        event = False
                        sequence = False
                        if diff > threshold and (time.time() - prev_event > event_wait):
                            event = True
                            if sequence_qty > 0 and sequence_counter == 0:
                                sequence_counter = sequence_qty
                        if sequence_counter > 0 and (time.time() - prev_event > sequence_wait): 
                            sequence = True
                            sequence_counter -= 1
                            if sequence_counter < 0:
                                sequence_counter = 0
                        if event or sequence:
                            call_alarm('movement', event_time, '%s-%s=%s' %(int(curr), int(prev), int(diff)))
                            prev_event = time.time()
                    prev = curr
                if frame_interval > 0:
                    show_text('Camera off')
                elif (time.time() - prev_frame > 1):
                    prev_frame = time.time() 
                    frame_rate = frame_counter
                    frame_counter = 0
                    if frame_rate != prev_frame_rate:
                        show_text(('Camera frame rate is %s fps') % frame_rate)
                        prev_frame_rate = frame_rate
            if frame_interval == 0:
            show_text(' - Camera finished.')

def temperature():
    #  Reads an optional temperature sensor
    global deg
    # Initialise variables
    temp_sensor = ''
    # Check whether a temperature sensor is installed    
    os.system('modprobe w1-gpio')    
    os.system('modprobe w1-therm')
    if os.path.exists(device_directory):
        dirs = os.listdir(device_directory)    
        for dir in dirs:    
            if dir[:2] == "28":    
                temp_sensor = dir
        show_text('* Incorrect temperature sensor device directory (' + device_directory + ').')
    if temp_sensor == '':    
        show_text('* No temperature sensor found.')  
        show_text('* Temperature sensor ' + temp_sensor + ' is installed.') 

        while system_active:
            if temp_sensor != '':
                sensor_file = device_directory + temp_sensor + '/w1_slave'
                if os.path.exists(sensor_file):
                    f = open(sensor_file, 'r')
                    lines = f.readlines()

                    if lines[0].strip()[-3:] == 'YES':
                        t = lines[1].find('t=')
                        prev_deg = deg
                        deg = '%sC'  % format(float(lines[1].strip()[t+2:])/1000 + temp_adj, '.1f')
                        if deg != prev_deg:
                            show_text('Temperature now ' + deg)
            for i in range(temp_interval):            
                if system_active:
        show_text(' - Temperature checker finished.')

def reactor():
    #  Checks for various alarm states and live update requirements once per second and updates log file
    prev_event = time.time()
    prev_timed = time.time()
    prev_regular = time.time()
    prev_temp = time.time()
    prev_trigger = time.time()
    prev_log = time.time()
    prev_live = time.time()
    low_temp = False
    high_temp = False
    low_light = False
    stop = False

        while system_active and not stop:
            event_time = time.strftime('%Y-%m-%d %H%M:%S')
            # Trigger detection
            if trigger_interval > 0 and GPIO.input(gpio_pin) == GPIO.LOW and (time.time() - prev_trigger > trigger_interval):
                prev_trigger = time.time()
                call_alarm('trigger', event_time, '')
            # Regular detection
            if regular_interval > 0 and (time.time() - prev_regular > regular_interval):
                prev_regular = time.time()
                call_alarm('regular', event_time, '')
            # Timed detection
            if time.strftime('%H%M%S') in timed_times and (time.time() - prev_timed > 1):
                prev_timed = time.time()
                call_alarm('timed', event_time, '')
            # Temperature detection
            if deg != '':
                degrees = float(deg[:-1])
                if degrees < min_temp and not low_temp:
                    call_alarm('low-temp', event_time, '%s' %deg)
                    low_temp = True
                if degrees >= min_temp:
                    low_temp = False
                if degrees > max_temp and not high_temp:
                    call_alarm('high-temp', event_time, '%s' %deg)
                    high_temp = True
                if degrees <= max_temp:
                    high_temp = False
            # Log file upload
            log_str = ''
            for event in event_log:
                log_str += event + '<br />'
            with open(live_directory + 'log.html','w') as f:
            if (log_interval > 0 and time.time() - prev_log > log_interval):
                prev_log = time.time()
                pickleitem("uploads.pickle", 'log.html')
                show_text('Log file ready to upload')
            # Live image upload
            if (live_interval > 0 and time.time() - prev_live > live_interval):
                prev_live = time.time()
                pickleitem("uploads.pickle", 'live.jpg')
                show_text('Live image ready to upload')
            # Restart if camera has failed
            if not ti.isAlive():
                call_alarm('camera stopped', event_time, '')
                os.system('shutdown -r +1')  # wait one minute to allow an email to be sent
                stop = True  #  stop this thread to prevent repeated shutdown calls
            time.sleep(1) # Reduces processor loading
        show_text(' - Event checker finished.')

def uploader():
    if (len(upload_actions) > 0 or settings_image or log_interval > 0) and ftp_server != '':
            while system_active:
                # Check for files which haven't yet been uploaded
                with open(live_directory + 'uploads.pickle', 'rb') as f:
                        upl_data = pickle.load(f)
                    except EOFError:
                        upl_data = list()
                    for item in upl_data:
                            if item == 'live.jpg':
                                itemname = live_image
                                uploadname = ftp_directory + 'live/live.jpg'
                            elif item == 'log.html':
                                itemname = live_directory + 'log.html'
                                uploadname = ftp_directory + 'live/log.html'
                                itemname = img_directory + item
                                uploadname = ftp_directory + 'camimages/' + item
                            file = open(itemname,'rb')
                            file_exists = True

                        except IOError:
                            # Remove item from list because it doesn't exist
                            show_text('Cannot upload ' + item + ' - does not exist')
                            file_exists = False
                        if file_exists:
                                if use_sftp:
                                    transport = paramiko.Transport(ftp_server, 22)
                                    transport.connect(username = ftp_user, password = ftp_pass)
                                    sftp = paramiko.SFTPClient.from_transport(transport)
                                    sftp.put(itemname, uploadname)
                                    session = ftplib.FTP(ftp_server, ftp_user, ftp_pass,'',20)
                                    session.storbinary('STOR ' + uploadname, file)
                                show_text('Uploaded ' + uploadname)
                                # Remove item from list
                                show_text('Failed to upload ' + uploadname)
                with open(live_directory + 'uploads.pickle', 'wb') as f:
                    pickle.dump(upl_data, f)
                for i in range(upload_interval):            
                    if system_active:
            show_text(' - Uploader finished.')        
def emailer():
    if len(email_actions) > 0:
            while system_active:
                if len(email_actions) > 0 and smtp_server != '':
                    # Check for images which haven't yet been emailed
                    with open(live_directory + 'emails.pickle', 'r+b') as f:
                            emails_data = pickle.load(f)
                        except EOFError:
                            emails_data = list()

                        for subject in emails_data:
                            #  Check whether subject is an image
                            imgname = img_directory + subject
                            if os.path.exists(imgname):
                                img = Image.open(imgname)
                                img.thumbnail((thumb_width, thumb_height), Image.ANTIALIAS)
                                tmpImage = cStringIO.StringIO()
                                img.save(tmpImage, "JPEG")
                                mesg = MIMEMultipart('related')
                                mesg['To'] = ", ".join(addr_to)
                                mesg['From'] = addr_from
                                mesg['Subject'] = sys_name + ' ' + subject
                                mesg.preamble = 'This is a multi-part message in MIME format.'

                                mesgAlternative = MIMEMultipart('alternative')
                                if (len(upload_actions) > 0 and ftp_server != ''):
                                    if ftp_url !='':
                                        mesgText = MIMEText('Please see ' + ftp_url + '?img=' + subject)
                                        link = "<a href='" + ftp_url + "?img=" + subject + "'>"
                                        mesgText = MIMEText(link + '<img src="cid:image1"></a><br>Click the thumbnail for the full-size image (internet access required).<br>Further images may be available.', 'html')
                                        mesgText = MIMEText('<img src="cid:image1"><br>The full-size version of this image has been uploaded.', 'html')
                                    mesgText = MIMEText('<img src="cid:image1"><br>The full-size version of this image has not been uploaded.', 'html')
                                mesgImage = MIMEImage(tmpImage.getvalue())
                                mesgImage.add_header('Content-ID', '<image1>')
                                # Create a basic text email message
                                mesg = MIMEText(time.strftime('%Y-%m-%d %H%M-%S') + ' ' + subject)
                                mesg['To'] = ", ".join(addr_to)
                                mesg['From'] = addr_from
                                mesg['Subject'] = sys_name + ' ' + subject
                            # Send the message
                                s = smtplib.SMTP(smtp_server, 587)
                                if encrypted:
                                    if s.has_extn('STARTTLS'):
                                        s.sendmail(addr_from, addr_to, mesg.as_string())
                                    s.sendmail(addr_from, addr_to, mesg.as_string())
                                 # Remove item from list
                                pickle.dump(emails_data, f)
                                show_text('Emailed ' + subject + ' to ' + mesg['To'] + '.')
                                show_text('Email failed')          
                for i in range(email_interval):            
                    if system_active:
            show_text(' - Emailer finished.')
def server():
    global httpd
    global port
    # Find the next available port if the specified one is busy
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    while port < 9999 and sock.connect_ex(('', port)) == 0:
        port = port + 1
    show_text('* Starting server on port %s.' % port) 
    loggedin = dict()
    doctype = '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/1999/REC-html401-19991224/strict.dtd">'

    def show_setup_page(self):
        html = doctype + '\n'
        html += '<html>\n'
        html += '<head>\n'
        html += '<title>%s setup</title>\n' % sys_name
        html += '<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">\n'
        html += '<style type="text/css">\n'
        html += 'body{font-family:sans-serif;}\n'
        html += '.heading{font-weight:bold;padding-top:20px;border-top:1px solid #000000;background:linear-gradient(#C0C0FF,#FFFFFF);}\n'
        html += '.help{font-size:0.7em;}\n'
        html += '</style>\n'
        html += '</head>\n'
        html += '<body>\n'
        html += '<form method="post" action="">\n'
        html += '\n<p class="heading">CAMERA</p>\n'
        html += get_input('width', '', 'Image width in pixels (security camera HiDef=928x576; D1=704x480). 1920x1080 does not use the full image sensor area.')
        html += get_input('height', '', 'Image height in pixels.')
        html += get_input('rotation', '0;90;180;270','Image rotation')
        html += get_input('lowlight', '', 'Light level below which camera automatically switches to long exposures (image size dependent, e.g. 30000000).')
        html += get_input('use_video', 'True;False', 'True = get video stream images (fastest); False = get direct')
        html += get_input('frame_interval', '', 'Seconds between frames (0 = continuous capture)')
        html += get_input('mode', 'off;auto;night;nightpreview;backlight;spotlight;sports;snow;beach;verylong;fixedfps;antishake;fireworks', 'Camera exposure mode')
        html += get_input('contrast', '', 'Contrast (default 0; range -100 to 100)')
        html += get_input('awb', 'off;auto;sunlight;cloudy;shade;tungsten;flourescent;incandescent;flash;horizon', 'Auto white balance')
        html += get_input('show_label', 'True;False', 'True = show system name and timestamp on image')
        html += get_input('show_label_back', 'True;False', 'True = show colour behind label')
        html += get_input('label_back', '', 'Colour of box behind label')
        html += get_input('show_size', 'True;False', 'Show the light level burnt into the image (for testing; normally False)')
        html += get_input('preview_mode', 'True;False', 'True = show picture on video and HDMI connectors')
        html += '\n<p class="heading">MOVEMENT DETECTION</p>\n'
        html += get_input('threshold', '', 'Image difference to trigger event in normal light; 0 = no movement detection')
        html += get_input('lowlight_threshold', '', 'Image difference to trigger event in low light; 0 = no lowlight movement detection')
        html += get_input('dark', '', 'Light level below which motion detection stops')
        html += get_input('move_type', 'inc;dec;null', 'Image size increase (''inc'') or decrease (''dec'') causes trigger - null for both.')
        html += get_input('event_wait', '', 'Seconds to wait before detecting the next normal light event')
        html += get_input('sequence_qty', '', 'Number of images in sequence for each normal light event')
        html += get_input('sequence_wait', '', 'Seconds between sequenced images')
        html += '\n<p class="heading">TIMED IMAGE CAPTURE</p>\n'
        html += get_input('timed_times', '', '[\'hhmmss\' \'hhmmss\'] etc = exact times to capture an image each day')
        html += '\n<p class="heading">REGULAR IMAGE CAPTURE</p>\n'
        html += get_input('regular_interval', '', 'Seconds between regular image captures; 0 = no regular captures')
        html += '\n<p class="heading">TRIGGERED IMAGE CAPTURE</p>\n'
        html += get_input('gpio_pin', '', 'Take this pin low to trigger')
        html += get_input('trigger_interval', '', 'Seconds between tests for gpio trigger; 0 = no gpio triggers')
        html += '\n<p class="heading">MEASUREMENT</p>\n'
        html += get_input('device_directory', '', 'System device directory')
        html += get_input('temp_adj', '', 'Temperature display adjustment')
        html += get_input('temp_interval', '', 'Number of seconds between temperature readings')
        html += get_input('min_temp', '', 'Displayed low temperature alarm level')
        html += get_input('max_temp', '', 'Displayed high temprature alarm level')
        html += '\n<p class="heading">WEB SERVER</p>\n'
        html += get_input('port', '', 'Lowest port number to serve on')
        html += get_input('password', '', 'Change as required')
        html += get_input('expiry', '', 'Seconds after which users are logged out (14400 is 4 hours - ignored if zero)')
        html += get_input('blacklist', '', 'IP addresses [\'address1\' \'address2\'] etc are not allowed to log in')
        html += get_input('whitelist', '', 'Only these IP addresses [\'address1\' \'address2\'] etc can log in (ignored if null)')
        html += get_input('maxusers', '', 'Maximum qty of simultaneous logins')
        html += get_input('latest_qty', '', 'Maximum number of images to list (reduce number if page loading is slow)')
        html += get_input('thumbqty', '', 'Maximum number of thumbnails to show)')
        html += get_input('startup', 'latest;thumbnails;live', 'What to show when the web page first loads')
        html += '\n<p class="heading">EMAILS</p>\n'
        html += get_input('email_actions', '', 'Actions which trigger emails [\'system starting\' \'movement\' \'low-light\' \'trigger\' \'timed\' \'regular\' \'low-temp\' \'high-temp\' \'camera stopped\']')
        html += get_input('email_interval', '', 'Minimum seconds between emails')
        html += get_input('smtp_server', '', 'Email server address (null = disable)')
        html += get_input('smtp_user', '', 'Email user name')
        html += get_input('smtp_pass', '', 'Email password ')
        html += get_input('encrypted', 'True;False', 'True = use TLS encrypted email')
        html += get_input('addr_from', '', 'Existing email account to send from')
        html += get_input('addr_to', '' ,'[\'address1\' \'address2\'] etc to send to')
        html += get_input('thumb_width', '', 'Email thumbnail width in pixels')
        html += get_input('thumb_height', '', 'Email thumbnail height in pixels')
        html += '\n<p class="heading">REMOTE SERVER</p>\n'
        html += get_input('ftp_server', '', 'FTP or SFTP server address (null = disable all uploads)')
        html += get_input('ftp_user', '', 'FTP/SFTP user name')
        html += get_input('ftp_pass', '', 'FTP/SFTP password')
        html += get_input('ftp_directory', '', 'Existing FTP/SFTP server directory to save to (must be null or end in /)')
        html += get_input('use_sftp', 'True;False', 'True = use SFTP rather than FTP')
        html += get_input('upload_interval', '', 'Minimum seconds between uploads')
        html += get_input('ftp_url', '', 'URL included in email message text to link to uploads web page')
        html += get_input('upload_actions', '', 'Actions which trigger uploads [\'system starting\' \'movement\' \'low-light\' \'trigger\' \'timed\' \'regular\' \'low-temp\' \'high-temp\' \'camera stopped\'] (null = disable these uploads) - only effective if ftp_server is not null')
        html += get_input('settings_image', 'True;False', 'True = upload live/live.jpg whenever a remote settings file changes - only effective if ftp_server is not null')
        html += get_input('live_interval', '', 'Seconds between regular live/live.jpg uploads (0 = disable) - only effective if ftp_server is not null')
        html += get_input('log_interval', '', 'Seconds between regular live/log.html uploads (0 = disable) - only effective if ftp_server is not null')
        html += '\n<p class="heading">SYSTEM</p>\n'
        html += get_input('sys_name', '', 'Appears on camera label; in email subject and on web pages')
        html += get_input('maxsize', '', 'Maximum number of bytes allowed in img_directory')
        html += get_input('poll_interval', '', 'Seconds between remote control file checks - may be security risk if activated (0 = disable)')
        html += get_input('poll_url', '', 'URL of remote control check file')
        html += get_input('poll_limit', '', 'Reboot after this qty of failed attempts to reach poll_url (0 = disable)')
        html += get_input('reboot_times', '', 'Reboot the system each day at [\'hhmmss\' \'hhmmss\'] etc')
        html += get_input('version', '', '')
        html += '\n<p style="padding-top:20px;border-top:1px solid #000000;"><input type="submit" name="update" value="Update for this session only"></p>\n'
        html += '<p><input type="submit" name="update_and_save" value="Update now and save as settings.txt for future sessions"></p>\n'
        html += '<p><input type="submit" name="reboot" value="Reboot"></p>\n'
        html += '</form>\n'
        html += '</body></html>\n'
        f = StringIO()
        self.copyfile(f, self.wfile)
    def show_homepage(self):
        filelist = []
        for filename in os.listdir(img_directory):
        filelist.sort(reverse = True)
        if len(filelist) > latest_qty:
            del filelist[latest_qty:] # show only the most recent items
            truncated = 1
            truncated = 0
        #get log as a string
        log = ""
        for event in event_log:
            log += event + "<br />"
        html = doctype + '\n'
        html += '<html>\n'
        html += '<head>\n'
        html += '<title>Camera viewer</title>\n'
        html += '<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">\n'
        html += '<script type="text/javascript">\n'
        # convert listing to javascript array
        html += '    var listing = new Array();\n'
        for filename in filelist:
            html += '    listing.push("' + filename + '");\n'
        # convert python variables to javascript ones
        html += '    var startup = "%s";\n' % startup
        html += '    var thumbqty = %s;\n' % thumbqty
        html += '    var truncated = %s;\n' % truncated
        html += '    var latest_qty = %s;\n' % latest_qty
        html += '    var logs = "%s";\n' % log
        # add function to respond to setup button
        html += '    function gosetup(){\n'
        html += '        var win = window.open("setup", "_blank");\n'
        html += '        win.focus();\n'
        html += '    }\n'
        html += '</script>\n'
        html += '</head>\n'
        html += '<body style="font-family:sans-serif;">\n'
        # get web page template
        with open('camdisplay.html', 'rb') as f:
            html += f.read()
        # add setup button
        html += '<p><input type="button" value="Setup" onClick="gosetup()"></p>\n'
        html += ' </body></html>\n'

        f = StringIO()
        self.copyfile(f, self.wfile)
    def validate(ip,self):
        if(ip in blacklist or ((len(whitelist) > 0 and ip not in whitelist))):
            # This IP is not allowed access
            txt = doctype + '<html><head><title>Information</title></head><body>Sorry, access from ' + ip + ' is not permitted.</body></html>'
            f = StringIO()
            self.copyfile(f, self.wfile)
            return False
        elif(ip not in loggedin):
            if(len(loggedin) == maxusers):
                # No capacity left
                txt = doctype + '<html><head><title>Information</title></head><body>Sorry, maximum number of users has been reached. Please try later.</body></html>'
                f = StringIO()
                self.copyfile(f, self.wfile)
                return False
                # Show the login form (checked in POST section)
                html = doctype
                html += '<html><head><title>Security Check</title><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"></head>'
                html += '<body><form method="post" action="">'
                html += 'Please enter your security code: <input type="password" name="pwd"> <input type="submit" name="go" value="Go">'
                html += '</form></body></html>'
                f = StringIO()
                self.copyfile(f, self.wfile)
                return False
        elif(expiry > 0 and time.time() - loggedin[ip] > expiry):
            #log the user out if their time is up
            del loggedin[ip]
            return False
            return True
    # Create a multithreading server class
    class ThreadingServer(SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer):
        allow_reuse_address = True
    # Create an http server class to react to specific GET and POST requests
    class ServerHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
        def do_GET(self):
            if validate(self.client_address[0],self):  
                # Intercept requests for the home page
                if (self.path[1:] == '' or self.path[1:] == 'index.html'):
                # Intercept requests for setup page
                elif (self.path[1:14] == 'setup'):


        def do_POST(self):
            form = cgi.FieldStorage(

            if ('pwd' in form):
                inpwd = form['pwd'].value[0:len(password)]
                if (inpwd == password):
                    loggedin[self.client_address[0]] = time.time()
                    show_text(('*** %s logged in ***') % self.client_address[0])
                    if 'setup' in self.path:
            if ('update' in form or 'update_and_save' in form):
                # re-arrange form fields into correct order
                fieldlist = []
                for item in form:
                    if item[0:3] == 'upd' and item[0:6] != 'update':
                setfile = '' 
                for item in fieldlist:
                    pos = item.find('_')
                    field = item[pos+1:]
                    fieldval = form[item].value
                    line = ''
                        globals()[field] = int(fieldval)
                        line = int(fieldval)
                            globals()[field] = eval(fieldval)
                            line = eval(fieldval)
                            line = "'" + fieldval + "'"
                    setfile += field + ' = %s\n' % line
                if ('update_and_save' in form):
                    with open('/home/pi/settings.txt','w') as f:
                    show_text('Saved settings.txt.')
                # restart the camera
                frame_interval = 1
                frame_interval = 0
            if ('reboot' in form):
    httpd = ThreadingServer(("", port), ServerHandler)

    if not system_active:
        show_text(' - Web server finished.')

ti = threading.Thread(target=imager)
tt = threading.Thread(target=temperature)    
tr = threading.Thread(target=reactor)
tu = threading.Thread(target=uploader)
te = threading.Thread(target=emailer)
ts = threading.Thread(target=server)
tc = threading.Thread(target=controller) 

# Show startup messages
print ('\n\n--------------------------\n')
show_text(sys_name + ' ' + version)
print ('\n--------------------------\n')
print ('Use Ctrl C to stop.\n\n')

# Start threads

call_alarm('system starting', time.strftime('%Y-%m-%d %H%M:%S'), '')

# Enable threads to be closed safely by using Ctrl-C
    while True:
except KeyboardInterrupt:
    show_text('Closing down ...')
    system_active = False

Hold down the Ctrl key and press X, then Y, then Enter to save the changes.

Make this program runnable by typing:

sudo chmod +x ./camaction.py

and press Enter.

The camaction.py program requires an html and a javascript template file for its web page. These are separate from camaction.py so that the web page format can be easily identically reused on other servers. First create camdisplay.html by typing:

sudo nano ./camdisplay.html

and press Enter. Copy the following and paste it by clicking the right mouse key:

<form action="">
Show live image <select id="live_rate" onChange="show_live()" style="font-size:1em;" disabled>
<option value="" selected>never</option>
<option value="500">every 0.5 second (2 fps)</option>
<option value="1000">every second (1 fps)</option>
<option value="5000">every 5 seconds</option>
<option value="10000">every 10 seconds</option>
<option value="15000">every 15 seconds</option>
<option value="30000">every 30 seconds</option>
<option value="60000">every minute</option>
<option value="120000">every 2 minutes</option>
<option value="300000">every 5 minutes</option>
<option value="600000">every 10 minutes</option>
<option value="900000">every 15 minutes</option>
<option value="1800000">every half hour</option>
<option value="3600000">every hour</option>
 <span style="font-size:0.7em;">Scroll down for event log and image listing.</span>
 <input type="button" id="later" value="< Later" disabled onClick="golater()">
 <input type="button" id="earlier" value="Earlier >" disabled onClick="goearlier()">
 <input type="button" id="play_button" value="Play the last 10 >>" disabled onClick="animator()">
 <input type="button" id="thumbs" value="Thumbnails" disabled onClick="select_group(0)">

<div style="margin:0px;padding:10px;background-color:#000000;color:#FFFFFF;text-align:center;position:relative;">
    <img src="" id="livepic" style="display:none;">
    <div id="latest"></div>
    <div style="position:absolute;top:5px;left:5px;text-align:left;color:#FF0000;font-weight:bold;" id="msg"></div>

<div id="degrees" style='margin-top:20px;'></div>

<div id="logs" style="border:1px solid #000000;margin-top:10px;max-height:120px;padding:5px;font-size:0.7em;overflow:auto;"></div>

<div id="listing" style="margin-top:40px;"></div>

<p>&copy; 2017 All Rights Reserved<br><a href="http://activedatainfo.co.uk" target="_blank">activedatainfo.co.uk</a></p>

<script type="text/javascript" src="camdisplay.js"></script>

Hold down the Ctrl key and press X, then Y, then Enter to save the changes.

Then create camdisplay.js by typing:

sudo nano ./camdisplay.js

and press Enter. Copy the following and paste it by clicking the right mouse key:

// For use with Raspberry Pi camera viewer script from activedatainfo.co.uk.
// Requires listing, startup, thumbqty, truncated, logs and latest_qty to be set before calling.
// Uses a short-lived cookie to preserve screen status when refreshed.
// Works on Firefox, Chrome, Edge and Internet Explorer 9+.

var liveratebox = document.getElementById("live_rate");
var livebox = document.getElementById("livepic");
var showbox = document.getElementById("latest");
var msgbox = document.getElementById("msg");
var degreebox = document.getElementById("degrees");
var logbox = document.getElementById("logs");
var listbox = document.getElementById("listing");
var laterbox = document.getElementById("later");
var earlierbox = document.getElementById("earlier");
var play_button = document.getElementById("play_button");
var thumbbox = document.getElementById("thumbs");
var starts = new Array();
var start = 0;
var img = 0;
var display_type = "";
var framecounter = 0;
var curr_date = "";
//check context
if (typeof truncated !== 'undefined') {
    liveratebox.disabled = false;
if (truncated){
    var text = latest_qty + " most recent items (see the <a href=camimages target=_blank>camimages directory</a> for a full list) ..."
    var text = "Recent items ..."

//create date-segmented display list with buttons
var row = 0;
var qty = thumbqty;
var newdate = false;
var options = {weekday: "long", year: "numeric", month: "long", day: "numeric"};
for (i = 0; i < listing.length; i++) {
    if (listing[i].substring(0,10) != curr_date){
        var curr_date = listing[i].substring(0,10);
        var showdate  = new Date(curr_date);
        text += "<br><br><span style=font-weight:bold;>" + showdate.toLocaleDateString("en-US", options) + "</span>";
        newdate = true;
        newdate = false;
    if (qty == 0 || newdate){
        qty = thumbqty;
        text += "<br><input type=button onClick=select_group(" + row + ") value=" + (row+1) + " style=font-weight:bold;>";
        row ++;
    qty --;
    text += " <input type=button onClick=select_image(" + i + ") value=" + listing[i].substring(11,listing[i].length-4) + ">";
listbox.innerHTML = text;

//reset display
if(x = logs.lastIndexOf("Temperature now")){
    var degree = logs.substring(x);
    var y = degree.indexOf("<");
    degree = degree.substring(0,y);
    degreebox.innerHTML = degree.replace("C", "&deg;C");
logbox.innerHTML = logs.replace(/ALARM/g, "<span style=color:#FF0000;>ALARM</span>", 3);
logbox.scrollTop = logbox.scrollHeight;

var mode = "";
var cookies = document.cookie.split(";");
for(var i = 0; i < cookies.length; i++) {
    if(cookies[i].substring(0,5) == "mode="){
        mode = cookies[i].substring(5);
document.cookie = "mode=; expires=Thu, 01 Jan 1970 00:00:00 UTC;";

if(mode == "group"){
}else if(mode == "image"){
    if(startup == "live"){
        liveratebox.value = "10000";
    }else if(startup == "latest"){
    }else if(startup == "thumbnails"){

function refresher(){
    document.cookie = "mode=" + display_type; 
    window.location = window.location;

function select_group(start){
    display_type = "group";
    thumbbox.disabled = true;
    liveratebox.value = "";
    livebox.style.display = "none";
    msgbox.innerHTML = listing[starts[start]].substring(0,10) + "<br>" + (start+1);
    if (start == 0){
        laterbox.disabled = true;
        z = setInterval("refresher()", 120000);
        msgbox.innerHTML += " (refreshes every 2 min)";
        laterbox.disabled = false;
    if (start == starts.length - 2){
        earlierbox.disabled = true;
        earlierbox.disabled = false;
    var str = "";
    for (s = starts[start]; s < starts[start+1]; s++){
        str += "<img src=camimages/" + listing[s] + " style=width:300px;margin:2px;border:none; onClick=select_image(" + s + ") title=" + listing[s].substring(11,listing[s].length-4) + ">";
    showbox.innerHTML = str;

function select_image(selected){
    img = selected;
    display_type = "image";
    thumbbox.disabled = false;
    play_button.disabled = false;
    livebox.style.display = "none";
    if (typeof z !== "undefined"){
    msgbox.innerHTML = listing[img];
    if (img == 0){
        laterbox.disabled = true;
        z = setInterval("refresher()", 120000);
        msgbox.innerHTML += "<br>(latest - refreshes every 2 min)";
        laterbox.disabled = false;
    if (img < listing.length-1){
        earlierbox.disabled = false;
        earlierbox.disabled = true;
    showbox.innerHTML = "<img src=camimages/" + listing[img] + " title=" + listing[img].substring(11,listing[img].length-4) + ">";

function golater(){
    if(display_type == "group"){
        start --;
    if(display_type == "image"){
        img --;

function goearlier(){
    if(display_type == "group"){
        start ++;
    if(display_type == "image"){
        img ++;

function animator(){
    framecounter = 0;
    if(display_type == "group"){
        while (framecounter < 10 && start < starts.length-1){
            start ++;
            framecounter ++;
    if(display_type == "image"){
        while (framecounter < 10 && img < listing.length){
            img ++;
            framecounter ++;
    y = setInterval("animate()",750);

function animate(){    
    framecounter --;
    if (framecounter == 0){

function show_live(){
    display_type = "live";
    livebox.src = "live/live.jpg";
    showbox.innerHTML = "";
    play_button.disabled = true;
    earlierbox.disabled = true;
    laterbox.disabled = true;
    thumbbox.disabled = false;
    var rate = liveratebox.value;

    if (typeof x !== "undefined"){
        livebox.style.display = "none";
        msgbox.innerHTML = "";
    if (rate !== ""){
        x = setInterval("get_update()", rate);
        livebox.style.display = "inline";
        msgbox.innerHTML = "Live";
        if (typeof z !== "undefined"){

function get_update(){
    var date = new Date();
    livebox.src = "live/live.jpg?id=" + date.getTime();

Hold down the Ctrl key and press X, then Y, then Enter to save the changes.

If movement detection requires masking, so that only a part of the image is used for movement detection, create a mask image called mask.jpg and upload it to the Raspberry Pi's /home/pi directory using an FTP program. The image can be any size (it will be adjusted in camaction.py to fit the camera image) and should consist of white where movement is to be detected and black where it is not to be detected. Ignore if not required - the program will create its own version of mask.jpg when it first runs.

Test the camaction.py program by typing

sudo ./camaction.py

and press Enter. If there are no errors, a stream of messages about what the program is doing will appear and the camera image will be shown on the monitor/tv (if still attached) and on the web page. Hold down Ctrl and press C to stop the program running.

Settings can be changed by (a) editing camaction.py or (b) creating an optional additional file settings.txt or (c) going to the setup screen by clicking the Setup button at the bottom of the Raspberry Pi's web page. The settings.txt file can contain some or all of the settings in the same style as in camaction.py. For example:

sys_name = 'Door Cam'
width = 704
height = 480
password = 'secret'
email_actions = ['system starting', 'timed', 'low-temp', 'high-temp']

A full settings.txt file can easily be created by clicking the Update now and save settings.txt for future sessions button at the bottom of the setup screen

The restarter.sh program which follows will ensure that camaction.py will start shortly after the system boots up.

Turning off the camera led

To turn off the camera led, type

sudo nano /boot/config.txt

and press Enter. Copy and paste the following at the end of the listing:


Add these two lines to also turn off the Raspberry Pi led:


Hold down the Ctrl key and press X, then Y, then Enter to save the changes, then reboot.

Viewing the camera images

To see the saved images, type raspberrypi (or whatever the chosen name is) into any browser on the same network as the Raspberry Pi and enter the password when requested (default = pwd, change this by editing camaction.py or settings.txt).

A Raspberry Pi Zero with config.txt and cmdline.txt edited as outlined in Initial setup option 2 above can be viewed by typing raspberrypi.local into a browser running on the PC into which the Pi Zero is plugged.

If directly viewing the live camera image it may be necessary to set


in /boot/config.txt to make the HDMI output work.

The list of images will automatically refresh every two minutes if just the first block of thumbnails is showing or it can be updated at any time by refreshing the browser in the normal way. Use the 'Later' and 'Earlier' buttons at the top of the screen to navigate the groups of thumbnails or click a group number in the list below the thumbnails. The earlier 10 groups before the current one can be run as an animation by clicking the 'Play the last 10' button at the top of the screen (this works best if the images have been cached by already having been displayed once).

Click on any thumbnail or click an image name from the group list to see the full-size image. If the first one is selected (the latest) this will refresh every two minutes. The earlier 10 images before the current one can be run as an animation by clicking the 'Play the last 10' button at the top of the screen (as with groups, this works best if the images have been cached by already having been displayed once). Click the 'Thumbnails' button to return to the thumbnail groups.

To see the live image stream, select an update rate from the dropdown menu at the top of the screen. Return to the saved images by selecting 'never' from the dropdown.

The mode that the web page first starts in ('live', 'latest' or 'thumbnails') can be changed with the startup setting.

The web page also shows the current temperature (if a sensor is installed) and the system log as it was when the page was last refreshed.

When images that have been automatically uploaded to a remote server need to be viewed, copies of the html and javascript templates can be incorporated into a web page configured for the remote server's operating system. A PHP example is shown below. The appearance and functionality of this web page are identical to the Raspberry Pi version except that the live image maximum update rate will be determined by the Raspberry Pi live_interval setting and the log and temperature update rate will be determined by log_interval.

Example remote server PHP web page 'index.php':


$password = "pwd";


if(isset($_POST['pwd']) and $_POST['pwd']==$password){
    <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/1999/REC-html401-19991224/strict.dtd">
    <meta name="robots" content="noindex">
    <title>Security Check</title>
    <form method='post' action=''>
    Please enter your security code: <input type='password' name='pwd'> <input type='submit' name='go' value='Go'>
<?php }else{//do everything that follows

//get listing and perform disk clean up
$src_folder = "camimages";
$filemax = 50000000; // maximum number of bytes allowed in $src_folder
$latest_qty = 150;   // maximum number of images to list (reduce number if page loading is slow)
$startup = "latest"; // mode when first starting - 'live', 'latest' or 'thumbnails'
$thumbqty = 8;       // number of thumbnails to show

        if(substr($file,4,1)=="-" and substr($file,-4)==".jpg"){


// show only the most recent items
if (count($filelist)>$latest_qty){

//get log as a string
if (file_exists("live/log.html")){
    $log = file_get_contents("live/log.html");
	$log = "(Log not available)";
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/1999/REC-html401-19991224/strict.dtd">
<title>Camera viewer</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<script type='text/javascript'>
var listing = new Array();
<?php foreach($filelist as $filename){ echo "listing.push('".$filename."');\n";}?>
var startup = '<?php echo $startup;?>';
var thumbqty = <?php echo $thumbqty;?>;
var truncated = <?php echo $truncated;?>;
var latest_qty = <?php echo $latest_qty;?>;
var logs = '<?php echo $log;?>';
<body style="font-family:sans-serif;">
<?php include ("camdisplay.html")?>
<?php }?>

Reducing the power consumption

The camera can be switched from continous operation to intermittent operation by entering an inter-frame time in seconds in the frame_interval setting.

This is an example of approximate power consumption using camaction.py software with a Raspberry Pi Zero W, Mk2 camera and temperature sensor:

Note that the frame interval is the time between the end of taking one frame and the start of taking the next, not the time between the starts of frames, so the time taken to turn the camera on, create a frame then turn the camera off may need to be taken into account if precise timimgs are required.

Movement detection will be of limited value when using intermittent operation because the camera's image will be slightly different each time it starts up.

Further savings may be available by disabling various parts of the Raspberry Pi system but none have been tested because they seem likely to reduce flexibility.

Automatically restart the camera software

A small program can check every minute that the main program is still running ok.


sudo nano ./restarter.sh

and press Enter. Copy and paste the following:

if !(ps ax | grep -v grep | grep camaction.py > /dev/null)
  sudo /home/pi/camaction.py

Hold down the Ctrl key and press X, then Y, then Enter to save the changes. Make this runnable by typing

sudo chmod +x ./restarter.sh

and press Enter. Add a line to the cron table to run this every minute by typing

sudo crontab -e

and press Enter. Copy this line and paste it at the end of the existing text:

* * * * * sudo /home/pi/restarter.sh

Hold down the Ctrl key and press X, then Y, then Enter to save the changes. Note that if the Raspberry Pi camera has developed a problem then a full reboot may be required so this script could be rewritten to do a reboot instead.

Connecting a trigger

Opto-isolator installation example

Code is included in camaction.py to trigger an image save/upload/email when GPIO 23 is taken low, for example by a PIR movement detector. The safest way of connecting an external trigger to GPIO pins is via an inexpensive opto-isolator.

A dual opto-isolator can be carefully soldered directly to the Raspberry Pi GPIO header pins with the unused channel being used for a UPS facility if required. Pins 5, 6, 7 and 8 of the ILD74 type of opto-isolator (e.g. Maplin YY62) will connect directly to pins 14-16-18-20 of the Raspberry Pi, i.e. GND-GPIO23-GPIO24-GND. The opto-isolator input pins are left pointing upwards ready to take thin wires from the remote trigger.

When the PIR is triggered, it takes the opto-isolator input high which takes the opto-isolator output (and therefore the gpio pin) low.

Measuring temperature

The camaction.py program contains code to read the ambient temperature from a DS18B20 temperature sensor and display it burnt into the camera image.

A DS18B20 temperature sensor and a 4.7K resistor can be soldered to a thin, three-core flexible cable which can be carefully soldered directly to Raspberry Pi GPIO pins 1, 7 and 9 (3.3v, GPIO4 and GND) - see the picture above. Using wires is easier than soldering a DS18B20 directly to the GPIO pins and enables the sensor to be located away from heat-generating components.

To activate the sensor, enable the one-wire interface in raspi-config by typing

sudo raspi-config

then press Enter and select 'Interfacing Options' then '1-Wire'. Tab to 'Finish' and press Enter to reboot. Alternatively, edit the config.txt file by typing

sudo nano /boot/config.txt

then press Enter. Add this line at the end of the file if it's not already there:


Hold down the Ctrl key and press X, then Y, then Enter to save the changes and reboot for the change to take effect.

Each DS18B20 has its own unique reference number for which camaction.py will automatically detect and configure itself.

Managing the Raspberry Pi when it can't be directly accessed

In some setups, such as when using 3G with a private IP address at a remote location, the camera might normally be inaccessible from the internet or other networks meaning that live images can't be seen, ftp doesn't work to change settings and the system can't be manually rebooted without visiting the location. However, it is still possible to control it and to see 'live' images with just a little bit of extra effort.

First, a settings.txt file (see above) needs to be placed on a web server to which the system has access - the full url of this should be entered as the system's poll_url setting. If there are no settings to change then the file can be empty, it just needs to exist to prevent the generation of 404 errors.

Then the poll_interval setting needs to be set to an interval (say 60 minutes) which balances the use of what may be expensive data with flexibility of control. The system checks this remote settings file after each interval and if the file has been changed then the settings it contains are immediately applied, overwriting currently-active settings.

Immediately a settings.txt file has been successfully accessed, it is accessed a second time with the result of the first access appended to the url as '?result=xxxx' where xxxx is the status of the downloaded information. This enables the process to be monitored by checking the appropriate web server log file.

If the settings.txt file hasn't changed then it is ignored. Because the file's timestamp is checked before its content, it is only necessary to resave the file on the remote web server to trigger an update on the Raspberry Pi at the end of the next interval. The remote settings are not saved at the Raspberry Pi end.

The system waits for 30 seconds after first starting before the first poll to give time for the network connection to be established.

Some of the settings which may be useful for remote control are summarised below:

Action Setting and example Comment
Validate the settings file sys_name = 'RPi Camera' Required as a security measure. Must exactly match the existing sys_name setting for the settings.txt file to have any effect.
Reboot the Raspberry Pi reboot_times = ['174500']
Live image grab settings_image = True Uploads a new live.jpg every time that the settings.txt file's timestamp changes on the web server. Can be used to create a pseudo 'live' sequence by constructing a web page which repeatedly polls the uploaded live.jpg and resaves the settings file on the web server to trigger a new live.jpg upload.
Cancel update checking poll_interval = 0 Checking of settings.txt cannot be restarted remotely once this option has been activated. Use with care.

Additionally, once having set up a poll url, the system can be set to automatically reboot itself if its network connection fails - perhaps because of a wi-fi or router problem for example. Just set the poll_limit to something other than zero.

Using 3G instead of wi-fi

Sometimes, a camera needs to be installed out-of-range of a wi-fi network. This page describes how to set up 3G.

Protecting the Raspberry Pi from power loss

Frequently stopping the Raspberry Pi by cutting the power can lead to SDHC card damage. This page describes how to add a simple Uninterruptible Power Supply (UPS).

Information included on this site is a personal opinion for information only and no responsibility can be accepted for any consequences that may arise from using the information contained here. No support is available with the practical implementation of code examples. Images are copyright and should not be downloaded or linked without permission and may be removed or changed without notice. References to products and software packages do not imply endorsement and their respective owners'/authors' rights and trade marks are acknowledged.
This site may be withdrawn at any time without notice.

The Picamera code used in this project to manage the camera board is that published at http://picamera.readthedocs.org which is Copyright Dave Hughes (no connection with this project).

© 2017 All rights reserved

First published August 2016
Revised October 2016 to add SFTP option, RAM drive and awb setting
Revised January 2017 to add GPIO trigger, UPS, temperature sensor and SSH installation change
Revised February 2017 to combine previous separate scripts into single multi-threading program (v6)
Revised February 2017 to include startup email option (v7) and improved UPS
Revised March 2017 to provide more flexible email and upload options (v8)
Revised April 2017 to reference Pi Zero W and a 3G update (v9)
Revised April 2017 to fix timed and regular trigger bugs (v10)
Revised April 2017 to add remote management and power saving (v12)
Revised May 2017 to improve movement detection, miscellaneous bug fixes (v13)
Revised May 2017 to add flag to top of web page (v14)
Revised May 2017 to fix disk free space calculation error (v15)
Revised June 2017 to improve remote management and minor bug fixes (v17)
Revised July 2017 to improve web page (v18)
Revised August 2017 to speed up web page loading (v19)
Revised September 2017 to add setup web page (v20)
Revised October 2017 to add auto restart after camera failure (v21)
Revised November 2017 to add web page startup options and html and js templates (v22)

This website does not itself use cookies, however we may from time to time use third-party advertising companies to serve advertisements when you visit our website. These companies may use cookies to gather information about your visits to this and other websites in order to provide advertisements about goods and services of interest to you. You can view our advertiser's privacy policy and your choices about not having this information used by clicking here. You can contact us by clicking here.