src

Go monorepo.
Log | Files | Refs

commit 8b0d3b23c38ca11a2eb542e6fad0a649ed942372
parent f2812a4cd3a2196f76167ff5c9ec233f9f3072af
Author: dwrz <dwrz@dwrz.net>
Date:   Thu,  2 Feb 2023 14:15:39 +0000

Add 2023-02-01 entry

Diffstat:
Acmd/web/site/entry/static/2023-02-01/2023-02-01.html | 1005+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acmd/web/site/entry/static/2023-02-01/metadata.json | 6++++++
2 files changed, 1011 insertions(+), 0 deletions(-)

diff --git a/cmd/web/site/entry/static/2023-02-01/2023-02-01.html b/cmd/web/site/entry/static/2023-02-01/2023-02-01.html @@ -0,0 +1,1005 @@ +<a href="/static/media/1920/dwrz_20221023T155810_edit.jpg" > + <img class="img-center" src="/static/media/720/dwrz_20221023T155810_edit.jpg"> +</a> + +<p> + I live in a ground floor apartment, and want to keep an eye on the space and + the cats when I am away. A search for security camera systems that offered + timelapse recording, livestream viewing, and instrusion notifications was + unsatisfactory. Most consumer systems were either too expensive, had security + shortcomings, or lacked sufficient user control. As a result, I endend up + assembling a system that meets most of my needs. +</p> + +<p> + The solution I've settled on uses three + <a href="https://www.raspberrypi.org/"> Raspberry Pi</a>'s, each acting as a + server connected to a <a href="https://www.webcamerausb.com/">generic fisheye + USB camera</a>. A mix of open-source software and scripts provides a + password protected livestream served over HTTPS, timelapse recording, motion + and object detection, notifications, and remote storage. +</p> + +<p> + The cameras come with limitations and vulnerabilties, some shared with + consumer solutions, others unique to a home-brewed setup. But for my needs, + they have worked well, and I have appreciate their modularity, the ability to + repurpose hardware, and full control over the system and the data that it + generates. +</p> + +<p> + I was surprised by how quickly I could stand up a system of such disparate + parts — in terms of hardware and software — while writing little code of my + own. Putting these cameras together seemed to confirm some of the + <a href="https://en.wikipedia.org/wiki/Unix_philosophy#Origin">UNIX</a> + principles. It's been possible to connect components with just a few scripts + as glue. +</p> + +<p> + Any code referenced on this page should be available here: + <a href="https://code.dwrz.net/vigil/">https://code.dwrz.net/vigil/</a>. + I don't intend to keep code on this page up to date; it should only be used + for example and inspiration. +</p> + +<hr> + +<p> + What follows is a rough guide covering the basic components of the system. + It is not intended to be a step-by-step guide, though there is a chance it + might work as one. +</p> + +<h2>Hardware</h2> + +<a href="/static/media/1920/dwrz_20221022T213439.jpg" > + <img class="img-center" src="/static/media/720/dwrz_20221022T213439.jpg"> +</a> + +<p> + I've opted for the following: + <ul> + <li> + <a href="https://www.raspberrypi.com/products/raspberry-pi-400/">Raspberry + Pi 400</a> — easier to source and repurpose for my use cases. I would + have preferred a smaller device with more open hardware and USB ports, + but it was hard to find anything of comparable price. + </li> + <li> + <a + href="https://www.westerndigital.com/products/usb-flash-drives/sandisk-extreme-pro-usb-3-2"> + Sandisk SSD Flash Drive</a> — improves performance and reliability + compared to running the Raspberry Pi off of a MicroSD card. + </li> + <li> + <a + href="https://www.amazon.com/s?me=AVRDPNYMU6GNM&marketplaceID=ATVPDKIKX0DER"> + ELP 3.6mm FHD 180° IR Fisheye Camera</a> — can cover an entire room and + see in the dark. + </li> + <li><a href="https://www.amazon.com/stores/ULIBERMAGNET/page/22395264-96B4-4270-9CA4-518AED93106E">ULIBERMAGNET</a> Tripod Ball Head with Magnetic Base — used to hold and position the cameras.</li> + </ul> +</p> + +<h2>Server</h2> + +<img class="img-center" src="/static/media/vigil-diagram.svg"> + +<p> + I've used the default operating system for Raspberry Pi's, <a href="https://www.raspberrypi.com/software/">Raspberry Pi OS</a>, and this guide assumes that context. I won't cover the operating system installation and setup of an administrative user — documentation is available elsewhere. +</p> + +<p> + You should be mindful of the security of the servers themselves. This is a + <a href="https://www.theverge.com/2022/11/30/23486753/anker-eufy-security-camera-cloud-private-encryption-authentication-storage" title="Anker’s Eufy lied to us about the security of its security cameras">problem</a> that even <a href="https://www.bloomberg.com/news/articles/2021-03-09/hackers-expose-tesla-jails-in-breach-of-150-000-security-cams" title="Hackers Breach Thousands of Security Cameras, Exposing Tesla, Jails, Hospitals">commercial</a> <a href="https://www.washingtonpost.com/technology/2019/04/23/how-nest-designed-keep-intruders-out-peoples-homes-effectively-allowed-hackers-get/" title="How Nest, designed to keep intruders out of people’s homes, effectively allowed hackers to get in">offerings</a> <a href="https://www.consumerreports.org/home-security-cameras/wyze-didnt-completely-fix-security-camera-flaws-for-3-years-a3726294358/" title="Wyze Didn't Completely Fix Flaws in Security Cameras for 3 Years">have not handled well</a>. + Your personal circumstances will dictate the balance of features and security. +</p> + +<aside class="bordered br-yellow"> + <p> + When installing the operating system, you'll have to decide whether to + encrypt the filesystem (or a partition). Encryption makes it harder to + extract data if someone is able to gain physical access to the + storage device. However, entering the decryption passwords requires you to + be physically present at the machine. This can be a problem if, e.g., a + transient power outage happens while you are away from the cameras; they + would cease to function until you are on premises again. It's possible to + use something like <code><a href="https://wiki.archlinux.org/title/Dm-crypt/Specialties#Remote_unlocking_(hooks:_netconf,_dropbear,_tinyssh,_ppp)">dropbear</a></code> for boot-time SSH, + to enter the decryption password. But I've opted to use the servers without + encryption, and transfer photos and videos off of the servers as soon as + possible. The downside is that any secrets on the device can be + compromised, requiring remediation in the event of a breach. + </p> +</aside> + +<p> + Once you are up and running with Raspberry Pi OS, ensure you are + using the latest software and security updates: + <pre><code>$ sudo apt-get -y update && sudo apt-get -y dist-upgrade</code></pre> +</p> + +<p> + Install any dependencies necessary to get work done, e.g.: + <pre><code>$ sudo apt-get install curl git mg</code></pre> +</p> + +<p> + Consider enabling <a href="https://wiki.debian.org/UnattendedUpgrades">unattended + security upgrades</a>: + + <pre><code>$ sudo apt-get install unattended-upgrades apt-listchanges </code></pre> +</p> + +<p> + To receive email reports for unattended upgrades, a + <a href="https://en.wikipedia.org/wiki/Message_transfer_agent"> + Message Transfer Agent + </a> (MTA) and the <code>mailx</code> command are required. This guide + assumes the use of <code><a href="https://marlam.de/msmtp/">msmtp</a></code>: + + <pre><code>$ sudo apt-get install bsd-mailx msmtp msmtp-mta</code></pre> +</p> + +<p> + Setup the <code>msmtp</code> + <a href="https://marlam.de/msmtp/msmtp.html#Configuration-files">configuration</a> + file for the <code>root</code> user: + + <pre><code>$ sudo mg /root/.msmtprc</code></pre> + + <pre><code>defaults +auth on +tls on +tls_trust_file /etc/ssl/certs/ca-certificates.crt +logfile /root/.msmtp.log + +account gmail +host smtp.gmail.com +port 587 +from user@example.com +user user@example.com +password ${PASSWORD} +# Alternatively, a command may be used to retrieve the password: +# passwordeval pass google/gmail/app +# See: https://marlam.de/msmtp/msmtp.html#passwordeval. + +account default : gmail</code></pre> +</p> +<p> + This configuration assumes a Gmail or Google Workspace account; you will + need to specify appropriate settings for your own mail provider. If you are + using Gmail or Google Workspaces, you will need to set up an "app password" + for programmatic access. +</p> + +<p> + Test that <code>msmtp</code> is working: + + <pre><code>echo "Test" | mailx -s "Test" user@example.com</code></pre> +</p> + +<p> + Edit the <code>unattended-upgrades</code> configuration to send email + notifications: + + <pre><code>$ sudo mg /etc/apt/apt.conf.d/50unattended-upgrades</code></pre> + + <pre><code>Unattended-Upgrade::Mail "user@example.com";</code></pre> +</p> + +<h2>Networking</h2> +<p> + I assign a static IP address for each of + my servers. Most consumer routers allow for this in their web interface; I + have something like the following in my router's <code>/etc/dhcpd.conf</code>: + <pre><code>host kitchen { + fixed-address 10.0.1.101; + hardware ethernet de:ad:be:ef:8d:8e; +}</code></pre> + + <aside class="bordered br-yellow"> + <p> + If you are using <code>network-manager</code> with a WiFi connection, + you'll need to disable MAC address randomization, or your router / DHCP + server will not be able to consistently match to the device. + + <pre><code>$ sudo mg /etc/NetworkManager/conf.d/wifi_rand_mac.conf</code></pre> + + <pre><code>[device] +wifi.scan-rand-mac-address=no</code></pre> + </p> + </aside> +</p> + +<p> + Set up a firewall; I block all ports by default, and at most leave three ports + open: one for <code>SSH</code>, one for the camera livestream (e.g., + <code>3000</code>), and optionally one for the web interface (e.g., + <code>8080</code>).</p> + +<p> + If you intend to share the livestream over the internet, you'll need a relay + or a port forward from your router. Depending on your network setup, you may + need to enable hairpin NAT or split-horizon DNS to access the servers by their + domain name when on the local network. +</p> + +<aside class="bordered br-yellow"> + <p> + I don't recommend allowing for remote access to web interface; and would + advise limiting access to the livestream if possible. <code>motion</code> is + not written in a memory safe language; all security cameras are a + double-edged sword that can compromise your own privacy. + </p> +</aside> + +<h2>SSH</h2> + +<p>Create an SSH keypair: + <pre><code>ssh-keygen -t ed25519</code></pre> +</p> + +<p> + Then, copy the public key to the server: + <pre><code>ssh-copy-id -i ${SSH_KEY_PATH} username@10.0.1.101 </code></pre> +</p> + +<p>Add the server to your SSH config: + <pre><code>Host kitchen + Hostname 10.0.1.101 + IdentityFile ~/.ssh/keys/kitchen</code></pre> +</p> + +<p> + On the server, disable root login and password authentication; enable public + key authentication. + + <pre><code>$ sudo mg /etc/ssh/sshd_config</code></pre> + + <pre><code>PermitRootLogin no +PubkeyAuthentication yes +PasswordAuthentication no</code></pre> + +</p> +<p> + Restart the SSH daemon: + <pre><code>$ systemctl restart sshd</code></pre> +</p> + +<h2>Motion</h2> +<p> + <code><a href="https://motion-project.github.io/">motion</a></code> provides + the core features of the system — multiple cameras, live streams, web + control, motion detection, saving images and movies, timelapse, and event + triggers. Install it: + <pre><code>$ sudo apt-get install motion</code></pre> +</p> + +<p> + On Raspberry Pi OS, the installation will create a + <code>motion</code> user, homed at <code>/var/lib/motion/</code>. The + configuration file for <code>motion</code> is located at + <code>/etc/motion/motion.conf</code>; the <code>systemd</code> unit file is + <code>/usr/lib/systemd/system/motion.service</code>. +</p> + +<p> + <code>motion</code>'s behavior is controlled via its configuration file. The + <a + href="https://motion-project.github.io/motion_config.html">documentation</a> + covers the settings, and should be reviewed; situation will dictate which + values to use. I discuss configuration below, after setting up the other + components of the system. +</p> + +<h2>Object Detection</h2> + +<a href="/static/media/1920/dwrz_20221026_1.jpg" > + <img class="img-center" src="/static/media/720/dwrz_20221026_1.jpg"> +</a> + +<p> + <code><a href="https://github.com/WongKinYiu/yolov7">yolov7</a></code> + provides the object detection functionality. I use the "tiny" weights for + faster processing on a Raspberry Pi. On a Raspberry Pi 400, I typically see + inferences complete under a second. + + <pre><code>$ sudo apt-get install git pip +$ sudo -u motion bash +$ git clone https://github.com/WongKinYiu/yolov7.git +$ cd yolov7/ +$ pip install -r requirements.txt +$ wget https://github.com/WongKinYiu/yolov7/releases/download/v0.1/yolov7-tiny.pt +$ mkdir -p /tmp/yolov7/</code></pre> +</p> + +<p> + <a href="https://aws.amazon.com/rekognition/">AWS Rekognition</a> can be used + as an alternative to <code>yolov7</code>. I had better — and cheaper — + results with <code>yolov7</code>. However, if you encounter any issues with + the <code>yolov7</code> installation, AWS offers a convenient fallback. +</p> + +<p> + To use Rekognition, you will need to setup an AWS account, install the + <code>aws</code> CLI, and ideally, create an IAM user with permissions + restricted to the Rekognition service. You will also need to + install <code><a href="https://stedolan.github.io/jq/">jq</a></code> to parse + the JSON response from AWS. + + <pre><code>$ sudo apt-get install jq +$ pip3 install --system awscli +$ sudo -u motion bash +$ aws configure</code></pre> +</p> + +<h2>Notifications</h2> + +<a href="/static/media/1920/dwrz_20230124T165950_edit.jpg" > + <img class="img-center" src="/static/media/720/dwrz_20230124T165950_edit.jpg"> +</a> + +<p> + I've set things up so that notifications are only sent when two smartphones + are not reachable on the network. The upside is less notifications (though + they'll sometimes come through if the device goes to sleep). If you go this + route, you'll need need to set up static IP addresses for your devices, and + remember to take them with you. +</p> + +<p> + I send SMS notifications by emailing my mobile phone number, setting the + recipient to something like <code>1234567890@msg.fi.google.com</code>. That + option may not be available depending on your mobile service provider. A + fallback would be to send notifications to an email address, or to use a + service like <a href="https://www.twilio.com/">Twilio</a> or + <a href="https://aws.amazon.com/sns/">AWS SNS</a>. +</p> + +<p> + I include the object-detected camera snapshot in notifications. While it's + not too difficult to write a script or simple program to compose + <a href="https://en.wikipedia.org/wiki/MIME">MIME</a> + emails, it's easier to just install + <code><a href="http://www.mutt.org/">mutt</a></code>. + + <pre><code>$ sudo apt-get install mutt</code></pre> +</p> + +<p> + Configure <code>msmtp</code> and <code>mutt</code> for the <code>motion</code> + user: + + <pre><code>$ sudo -u motion bash +$ cd ~ +$ mg .msmtprc</code></pre> + <pre><code>defaults +auth on +tls on +tls_trust_file /etc/ssl/certs/ca-certificates.crt +logfile ~/.cache/msmtp.log + +account gmail +host smtp.gmail.com +port 587 +from user@example.com +user user@example.com +password ${PASSWORD} + +account default : gmail</code></pre> + <pre><code>$ chmod 600 .msmtprc +$ mg .muttrc</code></pre> + <pre><code>set sendmail="/usr/bin/msmtp" +set use_from=yes +set from=user@example.com</code></pre> +</p> + +<h2>Exporting Data</h2> + +<p> + I've configured the system to delete snapshots after sending the + corresponding notification. This makes it harder to retrieve data if someone + gains access to the server. However, since I want to be able to review past + snapshots and timelapse footage, I backup the data off the servers. +</p> + +<p> + There are several options — <code>rsync</code> or <code>scp</code> files to + a remote server, perhaps one with an encrypted drive. Additionally, or + alternatively, the files can be backed up to the cloud, to a service like + <a href="https://aws.amazon.com/s3/">AWS S3</a> or <a + href="https://www.backblaze.com/b2/cloud-storage.html">Backblaze B2</a>. +</p> + +<p> + For <code>b2</code>, I took the following steps: + <ol> + <li>Create an account.</li> + <li>Create a bucket.</li> + <li> + Setup lifecycle rules on the bucket to delete files after a certain + number of days. + </li> + <li>Create an application key.</li> + </ol> +</p> +<p> + On the servers, I install and configure the <code>b2</code> CLI. + + <pre><code>$ sudo apt-get install backblaze-b2 +$ sudo -u motion bash +$ backblaze-b2 authorize-account</code></pre> +</p> + +<h2>DDNS</h2> +<p> + To make the camera stream available remotely and conveniently — without a + <code>VPN</code>, port forwarding, or dealing with IP addresses — I use + subdomains to reach my cameras. You will need your own domain for similar + functionality. +</p> + +<p> + I don't have a static IP from my ISP, so I use Dynamic DNS to keep my + subdomain records updated. A <code>systemd</code> timer regularly runs a + script to update the <code>A</code> and/or <code>AAAA</code> records for the + server's subdomain. +</p> + +<p> + The public IP of the server is retrieved with a DNS lookup, using + <code>dig</code>. On Raspberry Pi OS, you'll need to install the + <code>dnsutils</code> package: + + <pre><code>$ sudo apt-get intsall dnsutils</code></pre> +</p> + +<p> + How you update your records will depend on your registrar. I use + <a href="https://aws.amazon.com/route53/">AWS Route53</a>, and a simple Go + program I wrote called <code> + <a href="https://code.dwrz.net/src/file/cmd/r53/main.go.html"> + r53</a></code>, which wraps around the + <a href="https://github.com/aws/aws-sdk-go-v2/">AWS Go SDK</a> and + <code>dig</code>. +</p> + +<p> + A script is probably easier to install. The following isn't as full featured + as <code>r53</code>, but it doesn't require compiling and installing a Go + binary: + <pre><code>#!/usr/bin/env bash + +readonly HZ="${AWS_HOSTED_ZONE}" +readonly DOMAIN="${HOSTNAME}" + +err() { + echo "[$(date -u +'%Y-%m-%dT%H:%M:%S%:z')]: $*" >&2 +} + +main() { + if ! [[ -x "$(command -v aws)" ]]; then + err "aws cli not installed"; exit 1 + fi + + # Get the IP address. + ip="$(dig -4 +short myip.opendns.com @resolver1.opendns.com)" + if [[ -z "${ip}" ]]; then + err "failed to get ip address"; exit 2 + fi + printf "ip: %s\n" "${ip}" + + # Update the domains. + update='{ + "Comment": "DDNS", + "Changes": [ + { + "Action": "UPSERT", + "ResourceRecordSet": { + "Name": "'"${DOMAIN}"'", + "Type": "A", + "TTL": 300, + "ResourceRecords": [{ "Value": "'"${ip}"'" }] + } + } + ] +}' + + printf "requesting update for %s\n" "${DOMAIN}" + aws route53 change-resource-record-sets \ + --hosted-zone-id "${HZ}" \ + --change-batch "${update}" +} + +main "$@"</code></pre> +</p> + +<p> + If you are using AWS Route53, you'll need to install and setup the <code>aws</code> CLI for whichever user will run the DDNS service. Again, it's best to create an AWS IAM user with permissions limited to Route53. + + <pre><code>$ pip3 install --system awscli +$ sudo su +# aws configure</pre></code> +</p> + +<p> + <code>scp</code> the script or the <code>r53</code> binary to the server, + then move it and set appropriate permissions: + <pre><code>$ scp r53 user@server +$ sudo mv r53 /usr/local/bin/ +$ sudo chmod 755 /usr/local/bin/r53</code></pre> +</p> + +<p> + Test the command to ensure that it works: + <pre><code>$ r53 $HOSTNAME</code></pre> +</p> + +<p> + These are the <code>systemd</code> unit and timer files — install them at + <code>/usr/lib/systemd/system/</code>: + <pre><code>[Unit] +Description=DDNS +RefuseManualStart=no +RefuseManualStop=yes + +[Service] +Type=oneshot +ExecStart=ddns + +[Install] +WantedBy=timers.target</code></pre> + + <pre><code>[Unit] +Description=DDNS +RefuseManualStart=no +RefuseManualStop=no + +[Timer] +OnBootSec=1min +OnCalendar=*-*-* *:*/5:00 +Persistent=true +RandomizedDelaySec=15 +Unit=ddns.service + +[Install] +WantedBy=default.target</code></pre> +</p> +<p> + Then, enable the timer: + <pre><code>$ systemctl enable --now ddns.timer</code></pre> +</p> + +<a href="/static/media/1920/dwrz_20221018T191948_edit.jpg" > + <img class="img-center" src="/static/media/720/dwrz_20221018T191948_edit.jpg"> +</a> + +<h2>TLS Certificates</h2> + +<p> + <code>motion</code> will need TLS certificates to encrypt the livestream, + webcontrol, and authentication for each. We can get free certificates from <a + href="https://letsencrypt.org/">Let's Encrypt</a>, using <code><a + href="https://certbot.eff.org/">certbot</a></code>. The following assumes a + <code> + <a href="https://letsencrypt.org/docs/challenge-types/#dns-01-challenge"> + dns-01 + </a> + </code> challenge with Route53. +</p> + + +<p> + Install <code>certbot</code> and the <code>python3-certbot-dns-route53</code> + plugin: + + <pre><code>$ sudo apt-get install certbot python3-certbot-dns-route53</code></pre> +</p> + +<p> + Run <code>certbot</code> to generate certificates: + + <pre><code>$ sudo certbot certonly \ + --agree-tos \ + --email user@example.com \ + --non-interactive \ + --quiet \ + --verbose \ + --dns-route53 \ + -d ${DOMAIN}</code></pre> +</p> + +<p> + Add the <code>motion</code> user to the <code>ssl-cert</code> group. + + Then, change group ownership for access. + + <pre><code>$ chown -R root:ssl-cert letsencrypt/archive/${DOMAIN} +$ chown -R root:ssl-cert letsencrypt/live/${DOMAIN} +$ chmod 440 letsencrypt/archive/${DOMAIN}/privkey1.pem</code></pre> +</p> + +<a href="/static/media/1920/dwrz_20230104T104635_edit.jpg" > + <img class="img-center" src="/static/media/720/dwrz_20230104T104635_edit.jpg"> +</a> + +<h2>Motion Scripts</h2> + +<p> + We're nearly there. Three scripts are used to tie functionality together; + they should be copied over to <code>/var/lib/motion</code> and made + executable by the <code>motion</code> user. I use a + <a href="https://code.dwrz.net/vigil/file/setup.html">script</a> to make + installing the scripts a little easier. +</p> + +<p> + An <code>alert</code> script is called when motion is detected; it sends a + notification via email. + <pre><code>#!/usr/bin/env bash + +readonly RECIPIENT="${NOTIFICATION_RECIPIENT}" + +main() { + echo "${HOSTNAME}: motion detected at $(date '+%Y-%m-%dT%H:%M:%S%:z')." | \ + mutt -s "${HOSTNAME}: Motion Detected" \ + -- "${RECIPIENT}" +} + +main "$@"</code></pre> +</p> + +<p> + The <code>sync</code> script is used to backup timelapse videos: + <pre><code>#!/usr/bin/env bash + +readonly BUCKET="${B2_BUCKET}" + +main() { + local filepath="$1" + local name + name="$(basename "${filepath}")" + + backblaze-b2 upload-file \ + --threads 2 \ + "${BUCKET}" \ + "${HOME}/timelapse/${name}" \ + "${HOSTNAME}/timelapse/${name}" + + # Delete outdated files. + # This assumes the timelapse is created on an hourly basis. + rm -f "$1" + find "${HOME}/timelapse/" -mmin +60 -delete +} + +main "$@"</code></pre> +</p> + +<p> + The <code>notify</code> script sends notifications, and backs up the + snapshots: + + <pre><code>#!/usr/bin/env bash + +# Backblaze B2 Bucket +readonly BUCKET="${B2_BUCKET}" + +# Devices to check -- if populated and up, no notifications are sent. +readonly DEVICES=() + +# COCO Labels +readonly LABEL_PERSON=0 +readonly LABEL_CAT=15 + +# Lockfile to ensure that only one instance of the script is running. +readonly LOCKFILE="/tmp/motion-notify.lock.d" + +# yolov7 working directory. +readonly PROJECT="/tmp/yolov7" + +# Notification recipient. +readonly RECIPIENT="${NOTIFICATION_RECIPIENT}" + +acquire_lock () { + while true; do + if mkdir "${LOCKFILE}"; then + break; + fi + sleep 1 + done +} + +check_devices() { + for device in "${DEVICES[@]}"; do + if ping -c 1 -w 1 "${device}" &> "/dev/null"; then + return 0 + fi + done + + return 255 +} + +detect_objects() { + local filepath="$1" + + python "${HOME}/yolov7/detect.py" \ + --exist-ok \ + --no-trace \ + --save-txt \ + --project "${PROJECT}" \ + --name "motion" \ + --weights "${HOME}/yolov7/yolov7-tiny.pt" \ + --source "${filepath}" +} + +notify() { + local name="$1" + + echo "${HOSTNAME} at $(date '+%Y-%m-%dT%H:%M:%S%:z')" | \ + mutt -a "${PROJECT}/motion/${name}.jpg" \ + -s "${HOSTNAME}: Motion Detected" \ + -- "${RECIPIENT}" +} + +upload() { + local name="$1" + + backblaze-b2 upload-file \ + --threads 2 \ + "${BUCKET}" \ + "${PROJECT}/motion/${name}.jpg" \ + "${HOSTNAME}/photo/${name}.jpg" +} + +delete_outdated() { + local filepath="$1" + + acquire_lock + + rm -f "$1" + rm -f "${PROJECT}/motion/${name}.jpg" + find "${HOME}/photo/" -mmin +5 -delete + find "${PROJECT}/motion/" -iname "*.jpg" -mmin +5 -delete + find "${PROJECT}/motion/labels/" -mmin +5 -delete + + release_lock +} + +release_lock () { + rmdir "${LOCKFILE}" +} + +main() { + local filepath="$1" + local name + name="$(basename "${filepath}" .jpg)" + + # If devices are present, don't notify. + if (( "${#DEVICES[@]}" )); then + if check_devices; then + delete_outdated "${filepath}" "${name}" + exit 0 + fi + fi + + detect_objects "${filepath}" + + # Send a notification if we match any labels. + labels="$(awk '{print $1}' "${PROJECT}/motion/labels/${name}.txt")" + if echo "${labels}" | grep -qw "${LABEL_PERSON}\|${LABEL_CAT}"; then + notify "${name}" + fi + + upload "${name}" + + delete_outdated "${filepath}" "${name}" +} + +main "$@"</code></pre> + +</p> + +<p> + With AWS Rekognition, you'll need to adapt the script. The following will + handle uploading the image to AWS, and check if the labels are actionable: + + <pre><code>labels="$(env aws rekognition detect-labels \ +--min-confidence 90 \ +--image-bytes fileb://"${filepath}" \ +| jq -j '.Labels | .[] | "\n",.Name," ",.Confidence')" + +if grep --quiet "Human\|Cat" <<< "${labels}"; then + echo "${HOSTNAME} at $(date '+%Y-%m-%dT%H:%M:%S%:z')" | \ + mutt -a "${filepath}" \ + -s "${HOSTNAME}: Motion Detected" \ + -- "${RECIPIENT}" +fi</code></pre> +</p> + +<h2>Motion Config</h2> + +<p> + The last step is to configure <code>motion</code> to: + + <ul> + <li>Take snapshots on motion detection</li> + <li>Capture a timelapse — one photo per second, one file per hour, synced to the Backblaze B2</li> + <li>Serve webcontrol on port 8080 over HTTPS</li> + <li>Livestream on port 3000 over HTTPS</li> + <li>Notify on motion detection and send object-detected snapshots</li> + <li>Keep minimal amounts of data on the local drive</li> + </ul> +</p> + +<p> + <pre><code># GENERAL +daemon off +target_dir ${MOTION_DIR} +log_file ${MOTION_LOG_FILE} + +# IMAGE PROCESSING +despeckle_filter EedDl +framerate 24 +text_scale 2 +text_changes on +text_left %$ +text_right %Y-%m-%d-T%H:%M:%S %q + +# MOTION DETECTION +event_gap 1 +threshold 2000 + +# MOVIES +movie_output off +movie_filename /video/%Y-%m-%dT%H:%M:%S-%v + +# PICTURES +picture_output first +picture_filename /photo/%Y-%m-%dT%H-%M-%S_%q + +# TIMELAPSE +timelapse_interval 1 +timelapse_mode hourly +timelapse_fps 60 +timelapse_codec mpg +timelapse_filename /timelapse/%Y-%m-%d-%H-%M-%S + +# WEBCONTROL +webcontrol_auth_method 2 +webcontrol_authentication ${MOTION_USER}:${MOTION_PASSWORD} +webcontrol_port ${WEBCONTROL_PORT} +webcontrol_localhost off +webcontrol_cert ${TLS_CERT} +webcontrol_key ${TLS_KEY} +webcontrol_parms 0 +webcontrol_tls on + +# LIVE STREAM +stream_port ${STREAM_PORT} +stream_localhost off +stream_quality 25 +stream_motion on +stream_maxrate 24 +stream_auth_method 2 +stream_authentication ${MOTION_USER}:${MOTION_PASSWORD} +stream_preview_method 0 +stream_tls on + +# SCRIPTS +on_motion_detected ${MOTION_DIR}/alert +on_movie_end ${MOTION_DIR}/sync %f +on_picture_save ${MOTION_DIR}/notify %f + +# CAMERA +camera_name ${CAMERA_NAME} +videodevice /dev/video0 +height 1080 +width 1920</code></pre> +</p> + +<p>Restart <code>motion</code> to use the updated configuration: + <pre><code>$ systemctl restart motion</code></pre> +</p> + +<h2>Camera Management</h2> + +<p> + I use a simple script to manage the cameras. This examples allows control + over three servers (one of which has two cameras): + + <pre><code>#!/usr/bin/env bash + +readonly WEBCONTROL_PORT="${PORT_CONTROL}" + +readonly cameras=( + "https://${HOST0}:${WEBCONTROL_PORT}/0" +# "https://${HOST0}:${WEBCONTROL_PORT}/1" +# "https://${HOST1}:${WEBCONTROL_PORT}/${CAMERA0}" +# "https://${HOST2}:${WEBCONTROL_PORT}/${CAMERA0}" +) +readonly auth=( + "${MOTION_USER}:${MOTION_PASSWORD}" +# "${USER0}:${PW0}" +# "${USER1}:${PW1}" +# "${USER2}:${PW2}" +) + +err() { + echo "[$(date -u +'%Y-%m-%dT%H:%M:%S%:z')]: $*" >&2 +} + +main() { + local url="detection/status" + + case "$1" in + "capture"|"c") url="detection/snapshot" ;; + "pause"|"p") url="detection/pause" ;; + "start"|"s") url="detection/start" ;; + "status"|"") url="detection/status" ;; + *) err "unrecognized command: $1"; exit 1 + esac + + for i in "${!cameras[@]}"; do + curl --digest --user "${auth[i]}" "${cameras[i]}/${url}" + done +} + +main "$@"</code></pre> +</p> + +<h2>Next Steps</h2> + +<a href="/static/media/1920/dwrz_20220304T224024_edit.jpg" > + <img class="img-center" src="/static/media/720/dwrz_20220304T224024_edit.jpg"> +</a> + +<p> + As with all software, this project is a work-in-progress, at times + abandoned, and never completed. There are a few ideas I am exploring as I + continue to prototype the system: + + <ul> + <li> + The most urgent task is to make it easier to set up a new server and to + keep configuration consistent across servers. I'm working on minimizing + some of the duplicative work. Another option is to use containers. + </li> + <li> + Use <a href="https://www.openbsd.org/">OpenBSD</a> instead of Raspberry + Pi OS, replacing <code>msmtp</code> with <code>OpenSTMTPD</code>, + <code>acme-client</code> for <code>letsencrypt</code>. The main concern + here is whether wireless and <code>yolov7</code> are sufficiently + performant. + </li> + <li> + Use an in-memory filesystem and forgo the SSD, which might drop ~$30 + from the cost of the system. + </li> + <li> + Use different hardware — wireless or PoE security cameras with an RTSP + stream and motion running on a single server. + </li> + <li> + Improve object detection with <code>yolov7</code> by training the model. + </li> + <li> + Develop my own Go service to replace or wrap around <code>motion</code>, + or replace the bash scripts with Go programs. + </li> + <li> + Add features, like two-way communication. + </li> + <li> + Use <code>yolov7</code> to monitor the camera stream directly, and forgo + the motion detection step. + </li> + </ul> +</p> + +<p> + The final thanks must go to the open-source contributors who have made this + approach possible, from the operating system all the way up to the + interpreters. +</p> diff --git a/cmd/web/site/entry/static/2023-02-01/metadata.json b/cmd/web/site/entry/static/2023-02-01/metadata.json @@ -0,0 +1,6 @@ +{ + "cover": "dwrz_20221023T155810_edit.jpg", + "date": "2023-02-01T00:00:00Z", + "published": true, + "title": "Security Cameras" +}