Improving the automated hedgehog video capture
By Patrick Wigmore, , published: , updated
With working motion detection and the temperamental webcam forcibly subdued, it was time to iron out the wrinkles in the Hedgehog Home Hub recording set-up.
A slew of tweaks and improvements ensued.
SFTP server
At first, I installed the package openssh-sftp-server
to allow easy file browsing from my computer, but rsync
ended up being a better tool to use.
Daytime recording in colour
Unlike nighttime infrared recordings, daytime recordings benefit from being in colour. I added variables for the start and end of day mode recording and executed v4l2-ctl
with different parameters to specify black and white or colour, depending on the time of day.
File transfer improvements – rsync and 2.4GHz WiFi
rsync
proved to be an easier and more reliable way to transfer recordings off of the Hedgehog Home Hub.
I also had to switch the Hedgehog Cam to 2.4GHz WiFi, because 5GHz was too unreliable at the distance involved, and I was having to use a small whiteboard as a reflector next to my router, to improve the signal, which made a remarkable difference but did not stop the signal drifting in and out of usefulness.
Of course, I knew from the outset that 2.4GHz WiFi would have better range than 5GHz, but due to the idiosyncrasies of my existing network, 5GHz happened to be more convenient for me to set up, and had seemed to work initially.
Pre-caching to make USB flash drive directory listings faster
Directory listings on the USB flash drive were a bit slow, which also affected rsync. To speed things up, I figured out that running
find $RECORDING_DIR -iname "*.mjpg" 1>/dev/null
would cause the OS to cache the required data, speeding up subsequent operations. I slipped this command into hhcam-power-timer
(code listed further below) and hhcam-timer
:
#!/bin/sh
# Script to end a hedgehog camera recording after a certain time
# The first argument should be the path to a file containing the PID of the
# recording process that will be stopped after the time.
# The second argument should be the time after which to end the recording
# It should be formatted suitably for use as an argument for 'sleep'
# The third argument should be the path to a file containing the PID of this
# timer script once it is running. The script will remove it when it is done.
# Although the time may be specified in minutes, it needs to be timed to the
# nearest second or better.
# Timer
sleep "$2"
# Stop recording
kill -SIGTERM $(cat "$1")
killall -q -SIGTERM v4l2-ctl
# Remove PID files
rm "$1"
rm "$3"
# Load recording directory contents into read cache, in case we terminated
# recording just after a hhcam-usbreset, resulting in an empty cache
# If the cache is empty, then this will take a few minutes.
# If the cache is populated, then this will take about a second.
find $RECORDING_DIR -iname "*.mjpg" 1>/dev/null
Eliminating daytime footage
Finding it a chore to trawl through hours of daytime footage of wood pigeons, the dog, and human feet, on the off-chance that something interesting was going on, I eventually stopped reviewing any footage during daylight hours.
Soon, that, too, became tiresome, because I would have to manually specify which files to download and which to delete.
I took advantage of the day and night recording modes to disable daytime recording by simply commenting out the daytime recording command, so that the script would run but make no recordings during the designated daytime hours. This significantly improved the situation, because I could simply copy extract all files that had been recorded, rather than having to pick through them and only take recordings from certain hours of the day.
But, with the days lengthening and the clocks going forward for British Summer Time, I found that the existing day mode was becoming inadequate for this purpose. I wanted to use the time of sunrise and sunset to determine when day mode began and ended.
Taking account of sunrise and sunset
There did not appear to be any simple utility that did precisely what I needed, so I wrote a small Python script using an existing library to calculate the sunrise and sunset times. I settled on using Python 3 with the astral module, since I am familiar with Python and it was already installed on OpenWRT. It’s a matter of a few lines to calculate sunrise and sunset times for a given latitude, longitude and elevation, using the astral module. These can then be used to say whether the sun is currently risen or set, which was the output I needed. More simply, you can calculate the angle of the sun above the horizon and test whether it is less than 0. If it is, then the sun is currently set. astral can also calculate other times, such as dawn, dusk and noon, but I wanted sunrise and sunset.
However, Python incurs a significant time penalty in returning the “risen/set” status of the sun using such a script, because it takes so long for the interpreter to load on the limited Home Hub hardware. It’s possible there might be a way to speed that up, but I decided that the easiest solution was to cache the sunrise and sunset times, along with a ’last updated’ date, into a temporary file in the form of shell variables, so that the temporary file could be sourced by a much faster-loading shell script, to gain access to the stored values.
The Python script, which I named hhcam-suntimes-gen
, would simply generate an up-to-date temporary file. The accompanying shell script; hhcam-sunstate
; checks whether the temporary file exists and whether it is for the current date. If so, it returns “set” or “risen” based on the current time. Otherwise, it runs hhcam-suntimes-gen
to generate a new temporary file. When a new file needs to be generated, the script can take around 5 seconds to return a result, whereas if the file already exists the return is in less than a tenth of a second. So, it is well worth having this two-part arrangement.
/sbin/hhcam-suntimes-gen
:
#!/usr/bin/python3
# Script to generate a temporary file containing the times of sunrise and sunset
# for today.
#
# For simplicity, I've hard-coded the location
from astral import Location
import datetime
TEMP_FILE_NAME = '/tmp/suntimes-tmp'
# Change this to suit other locations
# System time on the Home Hub is in UTC, so use that as time zone
l = Location(("London", "United Kingdom", 51.50733, -0.12765, "UTC", 10))
s = l.sun()
rise_h = s["sunrise"].strftime("%H")
rise_m = s["sunrise"].strftime("%M")
set_h = s["sunset"].strftime("%H")
set_m = s["sunset"].strftime("%M")
dawn_h = s["dawn"].strftime("%H")
dawn_m = s["dawn"].strftime("%M")
dusk_h = s["dusk"].strftime("%H")
dusk_m = s["dusk"].strftime("%M")
today_str = datetime.date.today().strftime("%Y-%m-%d")
with open(TEMP_FILE_NAME, 'w') as sf:
sf.write('#!/bin/sh\n')
sf.write('# Note: times are in UTC\n')
sf.write('SUNTIMES_LAST_UPDATED=' + today_str + '\n')
sf.write('SUNRISE_H=' + rise_h + '\n')
sf.write('SUNRISE_M=' + rise_m + '\n')
sf.write('SUNSET_H=' + set_h + '\n')
sf.write('SUNSET_M=' + set_m + '\n')
sf.write('DAWN_H=' + dawn_h + '\n')
sf.write('DAWN_M=' + dawn_m + '\n')
sf.write('DUSK_H=' + dusk_h + '\n')
sf.write('DUSK_M=' + dusk_m + '\n')
(With hindsight, I’m not sure why I didn’t set the Hub up to use local time instead of UTC.)
/sbin/hhcam-libsunstate
:
#!/bin/sh
# Library of code for hhcam scripts related to sun state.
# This should match the value hard-coded in hhcam-suntimes-gen
ST_TEMP_FILE_NAME="/tmp/suntimes-tmp"
function check_cache () {
# Check whether the temp file exists
if [ -e $ST_TEMP_FILE_NAME ]; then
# If it exists, source it
source $ST_TEMP_FILE_NAME;
else
# If not, then, generate it with hhcam-suntimes-gen and try again
/sbin/hhcam-suntimes-gen || exit 1;
check_cache;
fi
}
/sbin/hhcam-sunstate
:
#!/bin/sh
# Script to say whether the sun has risen or set.
#
# Relies on hhcam-suntimes-gen to generate data.
#
# This part has to be in something faster than Python3, since any delay in
# returning a result will cause gaps in the video recording. (Python3 seems
# to take a few seconds to initialise before running scripts on the Home Hub 5a
# hardware.)
source /sbin/hhcam-libsunstate
function output_state () {
# If the temp file was generated today, then use its data
if [ $SUNTIMES_LAST_UPDATED == $(date -Idate) ]; then
# Output whether sun is risen or set
# Here, we're defining "set" to include the twilight of dawn and dusk.
# [Order of events: DAWN --> SUNRISE --> NOON --> SUNSET --> DUSK]
cur_h=$(date +%H)
cur_m=$(date +%M)
if [[ $cur_h -gt $SUNRISE_H && $cur_h -lt $SUNSET_H ]]; then
echo "risen"; exit 0;
else if [[ $cur_h -eq $SUNRISE_H && $cur_m -gt $SUNRISE_M ]]; then
echo "risen"; exit 0;
else if [[ $cur_h -eq $SUNSET_H && $cur_m -lt $SUNSET_M ]]; then
echo "risen"; exit 0;
else
echo "set"; exit 0;
fi; fi; fi
else
# If not, run hhcam-suntimes-gen to generate a new one and try again
/sbin/hhcam-suntimes-gen || exit 1;
check_cache;
output_state;
fi
}
check_cache;
output_state;
To minimise the occurrence of delays during video recording, a cron job was set up to pre-emptively run hhcam-suntimes-gen every night at midnight.
0 0 * * * /sbin/hhcam-suntimes-gen
This could probably be improved upon by storing the temporary files with the appropriate date in the name, and always generating the following day’s times as well as today’s, while deleting yesterday’s. That way, hhcam-sunstate would be pretty much guaranteed to find a file for the current date at all times, and the midnight cron job would primarily serve to remove yesterday’s temp file. However, for now, that seems like premature optimisation.
(Note from the future: I never found it necessary to optimise the suntimes generation further.)
Then, /sbin/hhcam-record
could read the sunstate and decide whether to record:
#!/bin/sh
# Script to record video from the hedgehog camera continuously
source /sbin/hhcam-parameters
STREAM_COUNT=$(awk "BEGIN {print int($FPS*60*$MAX_FILE_MINS)}")
disk_full_warning_file="$RECORDING_DIR"/00-DISK-FULL.txt
while true; do
# Wait for recording directory to be mounted by auto-mounter
if [ -d "$RECORDING_DIR" ]; then
# Determine whether disk is full
if [ $(df "$RECORDING_DIR" | grep "$RECORDING_DIR_DF_GREP_STRING"\
| awk '{ print $4 }') -lt $MIN_SPACE ]; then
# Loop record, or write file to indicate disk full error
if [[ "$LOOP_RECORDING" == "on" ]]; then
oldest_file=$(ls -1 "$RECORDING_DIR"/*.mjpg | sort | head -n1)
if [ -e "$oldest_file" ]; then
rm "$oldest_file"
# Need to "continue" to the next loop iteration at this
# point; i.e. skip the recording and to re-check how
# much free space is available; as it is quite likely
# that we will need to delete a great many very small or
# zero-sized files before enough space has been freed.
continue
else
echo "Less than $MIN_SPACE kB remaining on disk, but could not not determine oldest file to delete in loop recording."\
> "$RECORDING_DIR"/00-DISK-FULL.txt
exit 1
fi
else
if [ ! -e "$disk_full_warning_file" ]; then
echo "Less than $MIN_SPACE kB remaining on disk. Free up space to resume recording."\
> "$RECORDING_DIR"/00-DISK-FULL.txt
exit 1
fi
fi
fi
recording_datetime=$(date +%Y-%m-%dT%H%M%S)
if [ ! -c "$CAMERA_DEVICE" ];
then /sbin/hhcam-usbreset; sleep 2;
fi
if [[ $(/sbin/hhcam-sunstate) == "set" ]]; then
# Night mode recording
/usr/bin/v4l2-ctl \
-d "$CAMERA_DEVICE"\
-c saturation=0\
-p $FPS\
-v pixelformat=1,width=$WIDTH,height=$HEIGHT\
--stream-count $STREAM_COUNT\
--stream-poll\
--stream-user\
--stream-to-hdr "$RECORDING_DIR"/$recording_datetime.mjpg\
&> /dev/null
else
# Day mode recording
/usr/bin/v4l2-ctl \
-d "$CAMERA_DEVICE"\
-c saturation=100\
-p $FPS\
-v pixelformat=1,width=$WIDTH,height=$HEIGHT\
--stream-count $STREAM_COUNT\
--stream-poll\
--stream-user\
--stream-to-hdr "$RECORDING_DIR"/$recording_datetime.mjpg\
&> /dev/null
fi
if [ $? -ne 0 ]; then /sbin/hhcam-usbreset;
sleep 2;
fi
else
sleep 1
fi
done
Switching off the USB power during the daytime
Having reworked the code to use the sun state instead of the time, I realised it would not be much harder to add a utility to switch the USB power off during the daytime to save energy, if day mode recording is not switched on. This worked perfectly, with its own cron job running hhcam-power-timer
to turn it on at the start of the hour of sunset, and off at the end of the hour of sunrise.
By this point, /sbin/hhcam-power-timer
was looking fairly complicated:
#!/bin/sh
# Turns the USB power on or off depending on time of day (to save power)
# The IR floodlight runs all the time the USB power is turned on, so
# switching it off when it's not in use should save quite a bit of power.
# Ideally we would turn the floodlight on only when the PIR senses
# motion, for the duration of the recording. But there isn't a separate
# control wired in for the floodlight, so if the floodlight is turned off,
# then so will be the USB flash drive and the webcam. The flash drive and
# the webcam are too slow to initialise on-demand when a recording needs
# to start immediately. Therefore, we have to leave the power on during
# the hours when a recording could be triggered.
source /sbin/hhcam-libsunstate
source /sbin/hhcam-parameters
if [[ $DAY_MODE == "on" || $USB_DAYTIME_POWER == "on" ]]; then
# Switch on USB power and exit
echo "USB power is configured to be on all day."
echo "Switching on USB power"
/sbin/hhcam-power on
exit 0
fi
check_cache;
# We'll run this script on the hour, for the hour ahead
cur_h=$(date +%H)
# If it's dark, or will be dark by the end of the current hour
if [[ $cur_h -eq $SUNSET_H \
|| $cur_h -gt $SUNSET_H \
|| $cur_h -lt $SUNRISE_H \
|| $cur_h -eq $SUNRISE_H ]]; then
echo "It's nighttime."
echo "Switching on USB power"
/sbin/hhcam-power on
# Load recording directory contents into read cache by running find on it.
# First, wait 6 seconds for drive to auto-mount.
echo "Waiting 6 seconds for USB flash drive to auto-mount"
sleep 6
echo "Loading recording directory into read cache"
find $RECORDING_DIR -iname "*.mjpg" 1>/dev/null
else
echo "It's daytime."
# Don't switch the power off if rsync is running
if [[ "$(pgrep rsync | wc -l)" -ne 0 ]]; then
echo "But there's an rsync process running."
echo "Switching on USB power".
/sbin/hhcam-power on
exit 0
fi
# Unmount USB flash drive and switch off USB power
# If there are any lingering v4l2-ctl processes, those need terminating
# because they might prevent the filesystem from unmounting cleanly
if [[ "$(pgrep v4l2-ctl | wc -l)" -ne 0 ]]; then
killall -q -s TERM v4l2-ctl
# Unlike hhcam-usbreset, we are not in a hurry, so give them a good
# while to terminate properly before resorting to killing them.
timeout_secs=30
time_left=$timeout_secs
echo "Waiting $time_left seconds for v4l2-ctl processes to terminate..."
while [[ "$((--time_left))" -gt 0 && \
"$(pgrep v4l2-ctl | wc -l)" -ne 0 ]]; do
sleep 1
echo -e "\033[1A\r\033[KWaiting $time_left seconds for v4l2-ctl processes to terminate..."
done
if [[ "$(pgrep v4l2-ctl | wc -l)" -ne 0 ]]; then
echo -e "\033[1A\r\033[Kv4l2-ctl process(es) didn't terminate within $timeout_secs."
echo "Killing v4l2-ctl processes."
killall -q -s KILL v4l2-ctl
else
echo -e "\033[1A\r\033[Kv4l2-ctl rocess(es) terminated normally within $((timeout_secs-time_left)) seconds."
fi
fi
echo "Unmounting flash drive"
/bin/umount /mnt/hhusb || /bin/umount -f /mnt/hhusb || \
echo -e "\033[1A\r\033[KFlash drive did not unmount successfully."
echo "Switching off USB power"
/sbin/hhcam-power off
fi;
Automating the transfer and transcoding of recordings
I ended up writing a script called mjpg2mp4
, to identify mjpg files among the input, transcode them using ffmpeg with my preferred parameters, and output a summary of how many files were processed out of how many seen and what duration of video footage this amounted to.
Some of the recordings made by the Hedgehog Camera end up being zero-byte files. I think this happens when the webcam crashes, or possibly it happens if the timing of the recording is just barely longer than the configured limit for how long each recording file is. Either way, mjpg2mp4
deletes these zero byte files.
I also ended up writing an auto-fetch-and-convert
script, which would mount an 8GB ramdisk on my laptop, SSH into the Hedgehog Camera to turn the USB power on (if it wasn’t already), rsync
the files from the Hedgehog Camera into the ramdisk over the network, making a running log of which file names it had seen so that it could exclude them from subsequent file transfers, and then transcode all the files and move them to non-volatile storage.
The ramdisk was simply to save wear on my SSD or HDD, although it might also have helped with the transcoding speed.
sudo mount -t tmpfs -o size=8G pramdisk /home/patrick/tmp/ramdisk
rsync
has the capability to exclude certain files from the transfer, which I initially used. However, because the Hedgehog Home Hub’s processor is so slow, it ended up being faster to use find
on the Home Hub first, then pass the list of found files to rsync
s files-from
option.
latest_file="$(sort /home/patrick/hhcapture/exclude-mjpgs | tail -n 1)"
ssh root@hedgehog-hh5a.lan "find /mnt/hhusb/hhcapture -newer \"/mnt/hhusb/hhcapture/$latest_file\"" | awk -F / '{ print $NF }' | tail -n +2 > /home/patrick/hhcapture/new-files
rsync -t --progress --skip-compress=mjpg --files-from=/home/patrick/tmp/hhcapture/new-files root@hedgehog-hh5a.lan:/mnt/hhusb/hhcapture/ /home/patrick/tmp/ramdisk/
There is no point getting rsync
to compress anything on the Home Hub; you could probably transfer the uncompressed version in the time it takes to compress a file; so I used --skip-compress=mjpg
.
Lovely
Everything was going swimmingly. Then, the flash drive started acting up.