2023-02-01.html (30844B)
1 <a href="/static/media/1920/dwrz_20221023T155810_edit.jpg" > 2 <img class="img-center" src="/static/media/720/dwrz_20221023T155810_edit.jpg"> 3 </a> 4 5 <p> 6 I live in a ground floor apartment, and want to keep an eye on the space and 7 the cats when I am away. A search for security camera systems that offered 8 timelapse recording, livestream viewing, and instrusion notifications was 9 unsatisfactory. Most consumer systems were either too expensive, had security 10 shortcomings, or lacked sufficient user control. As a result, I endend up 11 assembling a system that meets most of my needs. 12 </p> 13 14 <p> 15 The solution I've settled on uses three 16 <a href="https://www.raspberrypi.org/"> Raspberry Pi</a>'s, each acting as a 17 server connected to a <a href="https://www.webcamerausb.com/">generic fisheye 18 USB camera</a>. A mix of open-source software and scripts provides a 19 password protected livestream served over HTTPS, timelapse recording, motion 20 and object detection, notifications, and remote storage. 21 </p> 22 23 <p> 24 The cameras come with limitations and vulnerabilties, some shared with 25 consumer solutions, others unique to a home-brewed setup. But for my needs, 26 they have worked well, and I have appreciate their modularity, the ability to 27 repurpose hardware, and full control over the system and the data that it 28 generates. 29 </p> 30 31 <p> 32 I was surprised by how quickly I could stand up a system of such disparate 33 parts — in terms of hardware and software — while writing little code of my 34 own. Putting these cameras together seemed to confirm some of the 35 <a href="https://en.wikipedia.org/wiki/Unix_philosophy#Origin">UNIX</a> 36 principles. It's been possible to connect components with just a few scripts 37 as glue. 38 </p> 39 40 <p> 41 Any code referenced on this page should be available here: 42 <a href="https://code.dwrz.net/vigil/">https://code.dwrz.net/vigil/</a>. 43 I don't intend to keep code on this page up to date; it should only be used 44 for example and inspiration. 45 </p> 46 47 <hr> 48 49 <p> 50 What follows is a rough guide covering the basic components of the system. 51 It is not intended to be a step-by-step guide, though there is a chance it 52 might work as one. 53 </p> 54 55 <h2>Hardware</h2> 56 57 <a href="/static/media/1920/dwrz_20221022T213439.jpg" > 58 <img class="img-center" src="/static/media/720/dwrz_20221022T213439.jpg"> 59 </a> 60 61 <p> 62 I've opted for the following: 63 <ul> 64 <li> 65 <a href="https://www.raspberrypi.com/products/raspberry-pi-400/">Raspberry 66 Pi 400</a> — easier to source and repurpose for my use cases. I would 67 have preferred a smaller device with more open hardware and USB ports, 68 but it was hard to find anything of comparable price. 69 </li> 70 <li> 71 <a 72 href="https://www.westerndigital.com/products/usb-flash-drives/sandisk-extreme-pro-usb-3-2"> 73 Sandisk SSD Flash Drive</a> — improves performance and reliability 74 compared to running the Raspberry Pi off of a MicroSD card. 75 </li> 76 <li> 77 <a 78 href="https://www.amazon.com/s?me=AVRDPNYMU6GNM&marketplaceID=ATVPDKIKX0DER"> 79 ELP 3.6mm FHD 180° IR Fisheye Camera</a> — can cover an entire room and 80 see in the dark. 81 </li> 82 <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> 83 </ul> 84 </p> 85 86 <h2>Server</h2> 87 88 <img class="img-center" src="/static/media/vigil-diagram.svg"> 89 90 <p> 91 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. 92 </p> 93 94 <p> 95 You should be mindful of the security of the servers themselves. This is a 96 <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>. 97 Your personal circumstances will dictate the balance of features and security. 98 </p> 99 100 <aside class="bordered br-yellow"> 101 <p> 102 When installing the operating system, you'll have to decide whether to 103 encrypt the filesystem (or a partition). Encryption makes it harder to 104 extract data if someone is able to gain physical access to the 105 storage device. However, entering the decryption passwords requires you to 106 be physically present at the machine. This can be a problem if, e.g., a 107 transient power outage happens while you are away from the cameras; they 108 would cease to function until you are on premises again. It's possible to 109 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, 110 to enter the decryption password. But I've opted to use the servers without 111 encryption, and transfer photos and videos off of the servers as soon as 112 possible. The downside is that any secrets on the device can be 113 compromised, requiring remediation in the event of a breach. 114 </p> 115 </aside> 116 117 <p> 118 Once you are up and running with Raspberry Pi OS, ensure you are 119 using the latest software and security updates: 120 <pre><code>$ sudo apt-get -y update && sudo apt-get -y dist-upgrade</code></pre> 121 </p> 122 123 <p> 124 Install any dependencies necessary to get work done, e.g.: 125 <pre><code>$ sudo apt-get install curl git mg</code></pre> 126 </p> 127 128 <p> 129 Consider enabling <a href="https://wiki.debian.org/UnattendedUpgrades">unattended 130 security upgrades</a>: 131 132 <pre><code>$ sudo apt-get install unattended-upgrades apt-listchanges </code></pre> 133 </p> 134 135 <p> 136 To receive email reports for unattended upgrades, a 137 <a href="https://en.wikipedia.org/wiki/Message_transfer_agent"> 138 Message Transfer Agent 139 </a> (MTA) and the <code>mailx</code> command are required. This guide 140 assumes the use of <code><a href="https://marlam.de/msmtp/">msmtp</a></code>: 141 142 <pre><code>$ sudo apt-get install bsd-mailx msmtp msmtp-mta</code></pre> 143 </p> 144 145 <p> 146 Setup the <code>msmtp</code> 147 <a href="https://marlam.de/msmtp/msmtp.html#Configuration-files">configuration</a> 148 file for the <code>root</code> user: 149 150 <pre><code>$ sudo mg /root/.msmtprc</code></pre> 151 152 <pre><code>defaults 153 auth on 154 tls on 155 tls_trust_file /etc/ssl/certs/ca-certificates.crt 156 logfile /root/.msmtp.log 157 158 account gmail 159 host smtp.gmail.com 160 port 587 161 from user@example.com 162 user user@example.com 163 password ${PASSWORD} 164 # Alternatively, a command may be used to retrieve the password: 165 # passwordeval pass google/gmail/app 166 # See: https://marlam.de/msmtp/msmtp.html#passwordeval. 167 168 account default : gmail</code></pre> 169 </p> 170 <p> 171 This configuration assumes a Gmail or Google Workspace account; you will 172 need to specify appropriate settings for your own mail provider. If you are 173 using Gmail or Google Workspaces, you will need to set up an "app password" 174 for programmatic access. 175 </p> 176 177 <p> 178 Test that <code>msmtp</code> is working: 179 180 <pre><code>echo "Test" | mailx -s "Test" user@example.com</code></pre> 181 </p> 182 183 <p> 184 Edit the <code>unattended-upgrades</code> configuration to send email 185 notifications: 186 187 <pre><code>$ sudo mg /etc/apt/apt.conf.d/50unattended-upgrades</code></pre> 188 189 <pre><code>Unattended-Upgrade::Mail "user@example.com";</code></pre> 190 </p> 191 192 <h2>Networking</h2> 193 <p> 194 I assign a static IP address for each of 195 my servers. Most consumer routers allow for this in their web interface; I 196 have something like the following in my router's <code>/etc/dhcpd.conf</code>: 197 <pre><code>host kitchen { 198 fixed-address 10.0.1.101; 199 hardware ethernet de:ad:be:ef:8d:8e; 200 }</code></pre> 201 202 <aside class="bordered br-yellow"> 203 <p> 204 If you are using <code>network-manager</code> with a WiFi connection, 205 you'll need to disable MAC address randomization, or your router / DHCP 206 server will not be able to consistently match to the device. 207 208 <pre><code>$ sudo mg /etc/NetworkManager/conf.d/wifi_rand_mac.conf</code></pre> 209 210 <pre><code>[device] 211 wifi.scan-rand-mac-address=no</code></pre> 212 </p> 213 </aside> 214 </p> 215 216 <p> 217 Set up a firewall; I block all ports by default, and at most leave three ports 218 open: one for <code>SSH</code>, one for the camera livestream (e.g., 219 <code>3000</code>), and optionally one for the web interface (e.g., 220 <code>8080</code>).</p> 221 222 <p> 223 If you intend to share the livestream over the internet, you'll need a relay 224 or a port forward from your router. Depending on your network setup, you may 225 need to enable hairpin NAT or split-horizon DNS to access the servers by their 226 domain name when on the local network. 227 </p> 228 229 <aside class="bordered br-yellow"> 230 <p> 231 I don't recommend allowing for remote access to web interface; and would 232 advise limiting access to the livestream if possible. <code>motion</code> is 233 not written in a memory safe language; all security cameras are a 234 double-edged sword that can compromise your own privacy. 235 </p> 236 </aside> 237 238 <h2>SSH</h2> 239 240 <p>Create an SSH keypair: 241 <pre><code>ssh-keygen -t ed25519</code></pre> 242 </p> 243 244 <p> 245 Then, copy the public key to the server: 246 <pre><code>ssh-copy-id -i ${SSH_KEY_PATH} username@10.0.1.101 </code></pre> 247 </p> 248 249 <p>Add the server to your SSH config: 250 <pre><code>Host kitchen 251 Hostname 10.0.1.101 252 IdentityFile ~/.ssh/keys/kitchen</code></pre> 253 </p> 254 255 <p> 256 On the server, disable root login and password authentication; enable public 257 key authentication. 258 259 <pre><code>$ sudo mg /etc/ssh/sshd_config</code></pre> 260 261 <pre><code>PermitRootLogin no 262 PubkeyAuthentication yes 263 PasswordAuthentication no</code></pre> 264 265 </p> 266 <p> 267 Restart the SSH daemon: 268 <pre><code>$ systemctl restart sshd</code></pre> 269 </p> 270 271 <h2>Motion</h2> 272 <p> 273 <code><a href="https://motion-project.github.io/">motion</a></code> provides 274 the core features of the system — multiple cameras, live streams, web 275 control, motion detection, saving images and movies, timelapse, and event 276 triggers. Install it: 277 <pre><code>$ sudo apt-get install motion</code></pre> 278 </p> 279 280 <p> 281 On Raspberry Pi OS, the installation will create a 282 <code>motion</code> user, homed at <code>/var/lib/motion/</code>. The 283 configuration file for <code>motion</code> is located at 284 <code>/etc/motion/motion.conf</code>; the <code>systemd</code> unit file is 285 <code>/usr/lib/systemd/system/motion.service</code>. 286 </p> 287 288 <p> 289 <code>motion</code>'s behavior is controlled via its configuration file. The 290 <a 291 href="https://motion-project.github.io/motion_config.html">documentation</a> 292 covers the settings, and should be reviewed; situation will dictate which 293 values to use. I discuss configuration below, after setting up the other 294 components of the system. 295 </p> 296 297 <h2>Object Detection</h2> 298 299 <a href="/static/media/1920/dwrz_20221026_1.jpg" > 300 <img class="img-center" src="/static/media/720/dwrz_20221026_1.jpg"> 301 </a> 302 303 <p> 304 <code><a href="https://github.com/WongKinYiu/yolov7">yolov7</a></code> 305 provides the object detection functionality. I use the "tiny" weights for 306 faster processing on a Raspberry Pi. On a Raspberry Pi 400, I typically see 307 inferences complete under a second. 308 309 <pre><code>$ sudo apt-get install git pip 310 $ sudo -u motion bash 311 $ git clone https://github.com/WongKinYiu/yolov7.git 312 $ cd yolov7/ 313 $ pip install -r requirements.txt 314 $ wget https://github.com/WongKinYiu/yolov7/releases/download/v0.1/yolov7-tiny.pt 315 $ mkdir -p /tmp/yolov7/</code></pre> 316 </p> 317 318 <p> 319 <a href="https://aws.amazon.com/rekognition/">AWS Rekognition</a> can be used 320 as an alternative to <code>yolov7</code>. I had better — and cheaper — 321 results with <code>yolov7</code>. However, if you encounter any issues with 322 the <code>yolov7</code> installation, AWS offers a convenient fallback. 323 </p> 324 325 <p> 326 To use Rekognition, you will need to setup an AWS account, install the 327 <code>aws</code> CLI, and ideally, create an IAM user with permissions 328 restricted to the Rekognition service. You will also need to 329 install <code><a href="https://stedolan.github.io/jq/">jq</a></code> to parse 330 the JSON response from AWS. 331 332 <pre><code>$ sudo apt-get install jq 333 $ pip3 install --system awscli 334 $ sudo -u motion bash 335 $ aws configure</code></pre> 336 </p> 337 338 <h2>Notifications</h2> 339 340 <a href="/static/media/1920/dwrz_20230124T165950_edit.jpg" > 341 <img class="img-center" src="/static/media/720/dwrz_20230124T165950_edit.jpg"> 342 </a> 343 344 <p> 345 I've set things up so that notifications are only sent when two smartphones 346 are not reachable on the network. The upside is less notifications (though 347 they'll sometimes come through if the device goes to sleep). If you go this 348 route, you'll need need to set up static IP addresses for your devices, and 349 remember to take them with you. 350 </p> 351 352 <p> 353 I send SMS notifications by emailing my mobile phone number, setting the 354 recipient to something like <code>1234567890@msg.fi.google.com</code>. That 355 option may not be available depending on your mobile service provider. A 356 fallback would be to send notifications to an email address, or to use a 357 service like <a href="https://www.twilio.com/">Twilio</a> or 358 <a href="https://aws.amazon.com/sns/">AWS SNS</a>. 359 </p> 360 361 <p> 362 I include the object-detected camera snapshot in notifications. While it's 363 not too difficult to write a script or simple program to compose 364 <a href="https://en.wikipedia.org/wiki/MIME">MIME</a> 365 emails, it's easier to just install 366 <code><a href="http://www.mutt.org/">mutt</a></code>. 367 368 <pre><code>$ sudo apt-get install mutt</code></pre> 369 </p> 370 371 <p> 372 Configure <code>msmtp</code> and <code>mutt</code> for the <code>motion</code> 373 user: 374 375 <pre><code>$ sudo -u motion bash 376 $ cd ~ 377 $ mg .msmtprc</code></pre> 378 <pre><code>defaults 379 auth on 380 tls on 381 tls_trust_file /etc/ssl/certs/ca-certificates.crt 382 logfile ~/.cache/msmtp.log 383 384 account gmail 385 host smtp.gmail.com 386 port 587 387 from user@example.com 388 user user@example.com 389 password ${PASSWORD} 390 391 account default : gmail</code></pre> 392 <pre><code>$ chmod 600 .msmtprc 393 $ mg .muttrc</code></pre> 394 <pre><code>set sendmail="/usr/bin/msmtp" 395 set use_from=yes 396 set from=user@example.com</code></pre> 397 </p> 398 399 <h2>Exporting Data</h2> 400 401 <p> 402 I've configured the system to delete snapshots after sending the 403 corresponding notification. This makes it harder to retrieve data if someone 404 gains access to the server. However, since I want to be able to review past 405 snapshots and timelapse footage, I backup the data off the servers. 406 </p> 407 408 <p> 409 There are several options — <code>rsync</code> or <code>scp</code> files to 410 a remote server, perhaps one with an encrypted drive. Additionally, or 411 alternatively, the files can be backed up to the cloud, to a service like 412 <a href="https://aws.amazon.com/s3/">AWS S3</a> or <a 413 href="https://www.backblaze.com/b2/cloud-storage.html">Backblaze B2</a>. 414 </p> 415 416 <p> 417 For <code>b2</code>, I took the following steps: 418 <ol> 419 <li>Create an account.</li> 420 <li>Create a bucket.</li> 421 <li> 422 Setup lifecycle rules on the bucket to delete files after a certain 423 number of days. 424 </li> 425 <li>Create an application key.</li> 426 </ol> 427 </p> 428 <p> 429 On the servers, I install and configure the <code>b2</code> CLI. 430 431 <pre><code>$ sudo apt-get install backblaze-b2 432 $ sudo -u motion bash 433 $ backblaze-b2 authorize-account</code></pre> 434 </p> 435 436 <h2>DDNS</h2> 437 <p> 438 To make the camera stream available remotely and conveniently — without a 439 <code>VPN</code>, port forwarding, or dealing with IP addresses — I use 440 subdomains to reach my cameras. You will need your own domain for similar 441 functionality. 442 </p> 443 444 <p> 445 I don't have a static IP from my ISP, so I use Dynamic DNS to keep my 446 subdomain records updated. A <code>systemd</code> timer regularly runs a 447 script to update the <code>A</code> and/or <code>AAAA</code> records for the 448 server's subdomain. 449 </p> 450 451 <p> 452 The public IP of the server is retrieved with a DNS lookup, using 453 <code>dig</code>. On Raspberry Pi OS, you'll need to install the 454 <code>dnsutils</code> package: 455 456 <pre><code>$ sudo apt-get intsall dnsutils</code></pre> 457 </p> 458 459 <p> 460 How you update your records will depend on your registrar. I use 461 <a href="https://aws.amazon.com/route53/">AWS Route53</a>, and a simple Go 462 program I wrote called <code> 463 <a href="https://code.dwrz.net/src/file/cmd/r53/main.go.html"> 464 r53</a></code>, which wraps around the 465 <a href="https://github.com/aws/aws-sdk-go-v2/">AWS Go SDK</a> and 466 <code>dig</code>. 467 </p> 468 469 <p> 470 A script is probably easier to install. The following isn't as full featured 471 as <code>r53</code>, but it doesn't require compiling and installing a Go 472 binary: 473 <pre><code>#!/usr/bin/env bash 474 475 readonly HZ="${AWS_HOSTED_ZONE}" 476 readonly DOMAIN="${HOSTNAME}" 477 478 err() { 479 echo "[$(date -u +'%Y-%m-%dT%H:%M:%S%:z')]: $*" >&2 480 } 481 482 main() { 483 if ! [[ -x "$(command -v aws)" ]]; then 484 err "aws cli not installed"; exit 1 485 fi 486 487 # Get the IP address. 488 ip="$(dig -4 +short myip.opendns.com @resolver1.opendns.com)" 489 if [[ -z "${ip}" ]]; then 490 err "failed to get ip address"; exit 2 491 fi 492 printf "ip: %s\n" "${ip}" 493 494 # Update the domains. 495 update='{ 496 "Comment": "DDNS", 497 "Changes": [ 498 { 499 "Action": "UPSERT", 500 "ResourceRecordSet": { 501 "Name": "'"${DOMAIN}"'", 502 "Type": "A", 503 "TTL": 300, 504 "ResourceRecords": [{ "Value": "'"${ip}"'" }] 505 } 506 } 507 ] 508 }' 509 510 printf "requesting update for %s\n" "${DOMAIN}" 511 aws route53 change-resource-record-sets \ 512 --hosted-zone-id "${HZ}" \ 513 --change-batch "${update}" 514 } 515 516 main "$@"</code></pre> 517 </p> 518 519 <p> 520 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. 521 522 <pre><code>$ pip3 install --system awscli 523 $ sudo su 524 # aws configure</pre></code> 525 </p> 526 527 <p> 528 <code>scp</code> the script or the <code>r53</code> binary to the server, 529 then move it and set appropriate permissions: 530 <pre><code>$ scp r53 user@server 531 $ sudo mv r53 /usr/local/bin/ 532 $ sudo chmod 755 /usr/local/bin/r53</code></pre> 533 </p> 534 535 <p> 536 Test the command to ensure that it works: 537 <pre><code>$ r53 $HOSTNAME</code></pre> 538 </p> 539 540 <p> 541 These are the <code>systemd</code> unit and timer files — install them at 542 <code>/usr/lib/systemd/system/</code>: 543 <pre><code>[Unit] 544 Description=DDNS 545 RefuseManualStart=no 546 RefuseManualStop=yes 547 548 [Service] 549 Type=oneshot 550 ExecStart=ddns 551 552 [Install] 553 WantedBy=timers.target</code></pre> 554 555 <pre><code>[Unit] 556 Description=DDNS 557 RefuseManualStart=no 558 RefuseManualStop=no 559 560 [Timer] 561 OnBootSec=1min 562 OnCalendar=*-*-* *:*/5:00 563 Persistent=true 564 RandomizedDelaySec=15 565 Unit=ddns.service 566 567 [Install] 568 WantedBy=default.target</code></pre> 569 </p> 570 <p> 571 Then, enable the timer: 572 <pre><code>$ systemctl enable --now ddns.timer</code></pre> 573 </p> 574 575 <a href="/static/media/1920/dwrz_20221018T191948_edit.jpg" > 576 <img class="img-center" src="/static/media/720/dwrz_20221018T191948_edit.jpg"> 577 </a> 578 579 <h2>TLS Certificates</h2> 580 581 <p> 582 <code>motion</code> will need TLS certificates to encrypt the livestream, 583 webcontrol, and authentication for each. We can get free certificates from <a 584 href="https://letsencrypt.org/">Let's Encrypt</a>, using <code><a 585 href="https://certbot.eff.org/">certbot</a></code>. The following assumes a 586 <code> 587 <a href="https://letsencrypt.org/docs/challenge-types/#dns-01-challenge"> 588 dns-01 589 </a> 590 </code> challenge with Route53. 591 </p> 592 593 594 <p> 595 Install <code>certbot</code> and the <code>python3-certbot-dns-route53</code> 596 plugin: 597 598 <pre><code>$ sudo apt-get install certbot python3-certbot-dns-route53</code></pre> 599 </p> 600 601 <p> 602 Run <code>certbot</code> to generate certificates: 603 604 <pre><code>$ sudo certbot certonly \ 605 --agree-tos \ 606 --email user@example.com \ 607 --non-interactive \ 608 --quiet \ 609 --verbose \ 610 --dns-route53 \ 611 -d ${DOMAIN}</code></pre> 612 </p> 613 614 <p> 615 Add the <code>motion</code> user to the <code>ssl-cert</code> group. 616 617 Then, change group ownership for access. 618 619 <pre><code>$ chown -R root:ssl-cert letsencrypt/archive/${DOMAIN} 620 $ chown -R root:ssl-cert letsencrypt/live/${DOMAIN} 621 $ chmod 440 letsencrypt/archive/${DOMAIN}/privkey1.pem</code></pre> 622 </p> 623 624 <a href="/static/media/1920/dwrz_20230104T104635_edit.jpg" > 625 <img class="img-center" src="/static/media/720/dwrz_20230104T104635_edit.jpg"> 626 </a> 627 628 <h2>Motion Scripts</h2> 629 630 <p> 631 We're nearly there. Three scripts are used to tie functionality together; 632 they should be copied over to <code>/var/lib/motion</code> and made 633 executable by the <code>motion</code> user. I use a 634 <a href="https://code.dwrz.net/vigil/file/setup.html">script</a> to make 635 installing the scripts a little easier. 636 </p> 637 638 <p> 639 An <code>alert</code> script is called when motion is detected; it sends a 640 notification via email. 641 <pre><code>#!/usr/bin/env bash 642 643 # Devices to check -- if populated and up, no notifications are sent. 644 readonly DEVICES=() 645 readonly RECIPIENT="${NOTIFICATION_RECIPIENT}" 646 647 check_devices() { 648 for device in "${DEVICES[@]}"; do 649 if ping -c 1 -w 1 "${device}" &> "/dev/null"; then 650 return 0 651 fi 652 done 653 654 return 255 655 } 656 657 main() { 658 # If devices are present, don't notify. 659 if (( "${#DEVICES[@]}" )); then 660 if check_devices; then 661 exit 0 662 fi 663 fi 664 665 echo "${HOSTNAME}: motion detected at $(date '+%Y-%m-%dT%H:%M:%S%:z')." | \ 666 mutt -s "${HOSTNAME}: Motion Detected" \ 667 -- "${RECIPIENT}" 668 } 669 670 main "$@"</code></pre> 671 </p> 672 673 <p> 674 The <code>sync</code> script is used to backup timelapse videos: 675 <pre><code>#!/usr/bin/env bash 676 677 readonly BUCKET="${B2_BUCKET}" 678 679 main() { 680 local filepath="$1" 681 local name 682 name="$(basename "${filepath}")" 683 684 backblaze-b2 upload-file \ 685 --threads 2 \ 686 "${BUCKET}" \ 687 "${HOME}/timelapse/${name}" \ 688 "${HOSTNAME}/timelapse/${name}" 689 690 # Delete outdated files. 691 # This assumes the timelapse is created on an hourly basis. 692 rm -f "$1" 693 find "${HOME}/timelapse/" -mmin +60 -delete 694 } 695 696 main "$@"</code></pre> 697 </p> 698 699 <p> 700 The <code>notify</code> script sends notifications, and backs up the 701 snapshots: 702 703 <pre><code>#!/usr/bin/env bash 704 705 # Backblaze B2 Bucket 706 readonly BUCKET="${B2_BUCKET}" 707 708 # Devices to check -- if populated and up, no notifications are sent. 709 readonly DEVICES=() 710 711 # COCO Labels 712 readonly LABEL_PERSON=0 713 readonly LABEL_CAT=15 714 715 # Lockfile to ensure that only one instance of the script is running. 716 readonly LOCKFILE="/tmp/motion-notify.lock.d" 717 718 # yolov7 working directory. 719 readonly PROJECT="/tmp/yolov7" 720 721 # Notification recipient. 722 readonly RECIPIENT="${NOTIFICATION_RECIPIENT}" 723 724 acquire_lock () { 725 while true; do 726 if mkdir "${LOCKFILE}"; then 727 break; 728 fi 729 sleep 1 730 done 731 } 732 733 check_devices() { 734 for device in "${DEVICES[@]}"; do 735 if ping -c 1 -w 1 "${device}" &> "/dev/null"; then 736 return 0 737 fi 738 done 739 740 return 255 741 } 742 743 detect_objects() { 744 local filepath="$1" 745 746 python "${HOME}/yolov7/detect.py" \ 747 --exist-ok \ 748 --no-trace \ 749 --save-txt \ 750 --project "${PROJECT}" \ 751 --name "motion" \ 752 --weights "${HOME}/yolov7/yolov7-tiny.pt" \ 753 --source "${filepath}" 754 } 755 756 notify() { 757 local name="$1" 758 759 echo "${HOSTNAME} at $(date '+%Y-%m-%dT%H:%M:%S%:z')" | \ 760 mutt -a "${PROJECT}/motion/${name}.jpg" \ 761 -s "${HOSTNAME}: Motion Detected" \ 762 -- "${RECIPIENT}" 763 } 764 765 upload() { 766 local name="$1" 767 768 backblaze-b2 upload-file \ 769 --threads 2 \ 770 "${BUCKET}" \ 771 "${PROJECT}/motion/${name}.jpg" \ 772 "${HOSTNAME}/photo/${name}.jpg" 773 } 774 775 delete_outdated() { 776 local filepath="$1" 777 778 acquire_lock 779 780 rm -f "$1" 781 rm -f "${PROJECT}/motion/${name}.jpg" 782 find "${HOME}/photo/" -mmin +5 -delete 783 find "${PROJECT}/motion/" -iname "*.jpg" -mmin +5 -delete 784 find "${PROJECT}/motion/labels/" -mmin +5 -delete 785 786 release_lock 787 } 788 789 release_lock () { 790 rmdir "${LOCKFILE}" 791 } 792 793 main() { 794 local filepath="$1" 795 local name 796 name="$(basename "${filepath}" .jpg)" 797 798 # If devices are present, don't notify. 799 if (( "${#DEVICES[@]}" )); then 800 if check_devices; then 801 delete_outdated "${filepath}" "${name}" 802 exit 0 803 fi 804 fi 805 806 detect_objects "${filepath}" 807 808 # Send a notification if we match any labels. 809 labels="$(awk '{print $1}' "${PROJECT}/motion/labels/${name}.txt")" 810 if echo "${labels}" | grep -qw "${LABEL_PERSON}\|${LABEL_CAT}"; then 811 notify "${name}" 812 fi 813 814 upload "${name}" 815 816 delete_outdated "${filepath}" "${name}" 817 } 818 819 main "$@"</code></pre> 820 821 </p> 822 823 <p> 824 With AWS Rekognition, you'll need to adapt the script. The following will 825 handle uploading the image to AWS, and check if the labels are actionable: 826 827 <pre><code>labels="$(env aws rekognition detect-labels \ 828 --min-confidence 90 \ 829 --image-bytes fileb://"${filepath}" \ 830 | jq -j '.Labels | .[] | "\n",.Name," ",.Confidence')" 831 832 if grep --quiet "Human\|Cat" <<< "${labels}"; then 833 echo "${HOSTNAME} at $(date '+%Y-%m-%dT%H:%M:%S%:z')" | \ 834 mutt -a "${filepath}" \ 835 -s "${HOSTNAME}: Motion Detected" \ 836 -- "${RECIPIENT}" 837 fi</code></pre> 838 </p> 839 840 <h2>Motion Config</h2> 841 842 <p> 843 The last step is to configure <code>motion</code> to: 844 845 <ul> 846 <li>Take snapshots on motion detection</li> 847 <li>Capture a timelapse — one photo per second, one file per hour, synced to the Backblaze B2</li> 848 <li>Serve webcontrol on port 8080 over HTTPS</li> 849 <li>Livestream on port 3000 over HTTPS</li> 850 <li>Notify on motion detection and send object-detected snapshots</li> 851 <li>Keep minimal amounts of data on the local drive</li> 852 </ul> 853 </p> 854 855 <p> 856 <pre><code># GENERAL 857 daemon off 858 target_dir ${MOTION_DIR} 859 log_file ${MOTION_LOG_FILE} 860 861 # IMAGE PROCESSING 862 despeckle_filter EedDl 863 framerate 24 864 text_scale 2 865 text_changes on 866 text_left %$ 867 text_right %Y-%m-%d-T%H:%M:%S %q 868 869 # MOTION DETECTION 870 event_gap 1 871 threshold 2000 872 873 # MOVIES 874 movie_output off 875 movie_filename /video/%Y-%m-%dT%H:%M:%S-%v 876 877 # PICTURES 878 picture_output first 879 picture_filename /photo/%Y-%m-%dT%H-%M-%S_%q 880 881 # TIMELAPSE 882 timelapse_interval 1 883 timelapse_mode hourly 884 timelapse_fps 60 885 timelapse_codec mpg 886 timelapse_filename /timelapse/%Y-%m-%d-%H-%M-%S 887 888 # WEBCONTROL 889 webcontrol_auth_method 2 890 webcontrol_authentication ${MOTION_USER}:${MOTION_PASSWORD} 891 webcontrol_port ${PORT_CONTROL} 892 webcontrol_localhost off 893 webcontrol_cert ${TLS_CERT} 894 webcontrol_key ${TLS_KEY} 895 webcontrol_parms 0 896 webcontrol_tls on 897 898 # LIVE STREAM 899 stream_port ${PORT_STREAM} 900 stream_localhost off 901 stream_quality 25 902 stream_motion on 903 stream_maxrate 24 904 stream_auth_method 2 905 stream_authentication ${MOTION_USER}:${MOTION_PASSWORD} 906 stream_preview_method 0 907 stream_tls on 908 909 # SCRIPTS 910 on_motion_detected ${MOTION_DIR}/alert 911 on_movie_end ${MOTION_DIR}/sync %f 912 on_picture_save ${MOTION_DIR}/notify %f 913 914 # CAMERA 915 camera_name ${CAMERA_NAME} 916 videodevice /dev/video0 917 height 1080 918 width 1920</code></pre> 919 </p> 920 921 <p>Restart <code>motion</code> to use the updated configuration: 922 <pre><code>$ systemctl restart motion</code></pre> 923 </p> 924 925 <h2>Camera Management</h2> 926 927 <p> 928 I use a simple script to manage the cameras. This examples allows control 929 over three servers (one of which has two cameras): 930 931 <pre><code>#!/usr/bin/env bash 932 933 readonly WEBCONTROL_PORT="${PORT_CONTROL}" 934 935 readonly cameras=( 936 "https://${HOST0}:${WEBCONTROL_PORT}/0" 937 # "https://${HOST0}:${WEBCONTROL_PORT}/1" 938 # "https://${HOST1}:${WEBCONTROL_PORT}/${CAMERA0}" 939 # "https://${HOST2}:${WEBCONTROL_PORT}/${CAMERA0}" 940 ) 941 readonly auth=( 942 "${MOTION_USER}:${MOTION_PASSWORD}" 943 # "${USER0}:${PW0}" 944 # "${USER1}:${PW1}" 945 # "${USER2}:${PW2}" 946 ) 947 948 err() { 949 echo "[$(date -u +'%Y-%m-%dT%H:%M:%S%:z')]: $*" >&2 950 } 951 952 main() { 953 local url="detection/status" 954 955 case "$1" in 956 "capture"|"c") url="detection/snapshot" ;; 957 "pause"|"p") url="detection/pause" ;; 958 "start"|"s") url="detection/start" ;; 959 "status"|"") url="detection/status" ;; 960 *) err "unrecognized command: $1"; exit 1 961 esac 962 963 for i in "${!cameras[@]}"; do 964 curl --digest --user "${auth[i]}" "${cameras[i]}/${url}" 965 done 966 } 967 968 main "$@"</code></pre> 969 </p> 970 971 <h2>Next Steps</h2> 972 973 <a href="/static/media/1920/dwrz_20220304T224024_edit.jpg" > 974 <img class="img-center" src="/static/media/720/dwrz_20220304T224024_edit.jpg"> 975 </a> 976 977 <p> 978 As with all software, this project is a work-in-progress, at times 979 abandoned, and never completed. There are a few ideas I am exploring as I 980 continue to prototype the system: 981 982 <ul> 983 <li> 984 The most urgent task is to make it easier to set up a new server and to 985 keep configuration consistent across servers. I'm working on minimizing 986 some of the duplicative work. Another option is to use containers. 987 </li> 988 <li> 989 Use <a href="https://www.openbsd.org/">OpenBSD</a> instead of Raspberry 990 Pi OS, replacing <code>msmtp</code> with <code>OpenSTMTPD</code>, 991 <code>acme-client</code> for <code>letsencrypt</code>. The main concern 992 here is whether wireless and <code>yolov7</code> are sufficiently 993 performant. 994 </li> 995 <li> 996 Use an in-memory filesystem and forgo the SSD, which might drop ~$30 997 from the cost of the system. 998 </li> 999 <li> 1000 Use different hardware — wireless or PoE security cameras with an RTSP 1001 stream and motion running on a single server. 1002 </li> 1003 <li> 1004 Improve object detection with <code>yolov7</code> by training the model. 1005 </li> 1006 <li> 1007 Develop my own Go service to replace or wrap around <code>motion</code>, 1008 or replace the bash scripts with Go programs. 1009 </li> 1010 <li> 1011 Add features, like two-way communication. 1012 </li> 1013 <li> 1014 Use <code>yolov7</code> to monitor the camera stream directly, and forgo 1015 the motion detection step. 1016 </li> 1017 </ul> 1018 </p> 1019 1020 <p> 1021 The final thanks must go to the open-source contributors who have made this 1022 approach possible, from the operating system all the way up to the 1023 interpreters. 1024 </p>