initial commit - untested (hardware pending)
This commit is contained in:
parent
991f6c3920
commit
f977240dfe
50
README.md
50
README.md
@ -1,3 +1,51 @@
|
|||||||
# storage-security
|
# storage-security
|
||||||
|
|
||||||
Security solution for my storage locker. Deployed to a raspberry pi with an attached camera
|
Security solution for my storage locker. Deployed to a raspberry pi with an attached camera.
|
||||||
|
|
||||||
|
### Technology Stack
|
||||||
|
|
||||||
|
* Raspberry Pi 4 w/ camera
|
||||||
|
* GoCV
|
||||||
|
* Syncthing
|
||||||
|
|
||||||
|
The raspberry pi is configured as a WLAN AP which my phone will connect to. My phone will also be running syncthing and have the RPI configured as a sync device. The phone will pull logs and videos taken from the RPI which have been saved to the sync folder each time my phone connects.
|
||||||
|
|
||||||
|
The same folder on my phone is also a syncthing destination with spud, so when I come back upstairs, it uploads it to my server.
|
||||||
|
|
||||||
|
This isn't a foolproof method in case the intruder locates the RPI / camera and disables / destroys it / removes it. The data is still stored on the RPI until the next time I'm within proximity. This is an acceptable risk given the constraints, however if a better method is discovered to immediately store the data outside of the storage unit that would be preferred (something low powered sitting in my vehicle? )
|
||||||
|
|
||||||
|
### Raspberry Pi Setup
|
||||||
|
|
||||||
|
Full steps to re-build this system are below.
|
||||||
|
|
||||||
|
##### Prerequisites
|
||||||
|
|
||||||
|
1. Connect the camera
|
||||||
|
2. Image the SDcard with Raspberry Pi OS Lite (minimal image based on debian) - make sure to pick lite - do not use the desktop version.
|
||||||
|
|
||||||
|
##### Boot optimizations
|
||||||
|
|
||||||
|
Edit /boot/config.txt
|
||||||
|
|
||||||
|
```conf
|
||||||
|
# Disable the rainbow splash screen
|
||||||
|
disable_splash=1
|
||||||
|
|
||||||
|
# Disable bluetooth
|
||||||
|
dtoverlay=pi3-disable-bt
|
||||||
|
|
||||||
|
# Set the bootloader delay to 0 seconds. The default is 1s if not specified.
|
||||||
|
boot_delay=0
|
||||||
|
```
|
||||||
|
|
||||||
|
Edit /boot/cmdline.txt to make kernel quiet. The following is an example, the key part is the quiet flag
|
||||||
|
|
||||||
|
```conf
|
||||||
|
dwc_otg.lpm_enable=0 console=serial0,115200 console=tty1 root=PARTUUID=32e07f87-02 rootfstype=ext4 elevator=deadline fsck.repair=yes quiet rootwait
|
||||||
|
```
|
||||||
|
|
||||||
|
Disable dhcpcd - useless service in this case
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo systemctl disable dhcpcd.service
|
||||||
|
```
|
127
main.go
Normal file
127
main.go
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
"image/color"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gocv.io/x/gocv"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
minimumMotionArea = 3000 // Motion detection minimum area needed to move
|
||||||
|
recordLengthAfterMotion = 30 // Number of seconds to keep recording going after motion was last detected
|
||||||
|
motionDetectInterval = 1 //number of seconds between motion detection attempts // TBD: Implement this, currently does nothing
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
lastMotionDetectedTime time.Time
|
||||||
|
currentRecording *gocv.VideoWriter
|
||||||
|
img, imgDelta, imgThresh gocv.Mat
|
||||||
|
mog2 gocv.BackgroundSubtractorMOG2
|
||||||
|
osdColor color.RGBA
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
if len(os.Args) < 2 {
|
||||||
|
fmt.Println("How to run:\n\tstorage-security [camera ID]")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
img = gocv.NewMat()
|
||||||
|
imgDelta = gocv.NewMat()
|
||||||
|
imgThresh = gocv.NewMat()
|
||||||
|
mog2 = gocv.NewBackgroundSubtractorMOG2()
|
||||||
|
osdColor = color.RGBA{0, 0, 255, 0}
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
defer img.Close()
|
||||||
|
defer imgDelta.Close()
|
||||||
|
defer imgThresh.Close()
|
||||||
|
defer mog2.Close()
|
||||||
|
|
||||||
|
// parse args
|
||||||
|
deviceID := os.Args[1]
|
||||||
|
|
||||||
|
webcam, err := gocv.OpenVideoCapture(deviceID)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Error opening video capture device: %v\n", deviceID)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer webcam.Close()
|
||||||
|
|
||||||
|
fmt.Printf("Start reading device: %v\n", deviceID)
|
||||||
|
|
||||||
|
// main loop
|
||||||
|
for {
|
||||||
|
if ok := webcam.Read(&img); !ok {
|
||||||
|
fmt.Printf("Device closed: %v\n", deviceID)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if img.Empty() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if detectMotion(img) {
|
||||||
|
// Determine if a new recording needs to start
|
||||||
|
if time.Now().After(lastMotionDetectedTime.Add(time.Second * recordLengthAfterMotion)) {
|
||||||
|
fileName := fmt.Sprintf("storage-%s.avi", time.Now().Format(time.RFC3339))
|
||||||
|
log.Printf("motion detected, started recording to file named %s", fileName)
|
||||||
|
currentRecording, err = gocv.VideoWriterFile(fileName, "MJPG", 25, img.Cols(), img.Rows(), true)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("error opening video writer device: %v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// And always update the timestamp
|
||||||
|
lastMotionDetectedTime = time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine if we are currently recording and if so, then save the frame to the video
|
||||||
|
if currentRecording != nil {
|
||||||
|
// OSD / timestamp
|
||||||
|
gocv.PutText(&img, time.Now().Format(time.RFC3339), image.Pt(10, 20), gocv.FontHersheyPlain, 1.2, osdColor, 2)
|
||||||
|
|
||||||
|
currentRecording.Write(img)
|
||||||
|
// Determine if we should stop recording
|
||||||
|
if lastMotionDetectedTime.Add(time.Second * recordLengthAfterMotion).After(time.Now()) {
|
||||||
|
log.Printf("motion has not been detected for the last %d seconds stopping recording to file", recordLengthAfterMotion)
|
||||||
|
err = currentRecording.Close()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("failed to close openCV file recording handle: %v", err)
|
||||||
|
}
|
||||||
|
currentRecording = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns true if motion detected in current frame
|
||||||
|
func detectMotion(frame gocv.Mat) bool {
|
||||||
|
// first phase of cleaning up image, obtain foreground only
|
||||||
|
mog2.Apply(frame, &imgDelta)
|
||||||
|
|
||||||
|
// remaining cleanup of the image to use for finding contours.
|
||||||
|
// first use threshold
|
||||||
|
gocv.Threshold(imgDelta, &imgThresh, 25, 255, gocv.ThresholdBinary)
|
||||||
|
|
||||||
|
// then dilate
|
||||||
|
kernel := gocv.GetStructuringElement(gocv.MorphRect, image.Pt(3, 3))
|
||||||
|
defer kernel.Close()
|
||||||
|
gocv.Dilate(imgThresh, &imgThresh, kernel)
|
||||||
|
|
||||||
|
// now find contours
|
||||||
|
contours := gocv.FindContours(imgThresh, gocv.RetrievalExternal, gocv.ChainApproxSimple)
|
||||||
|
for _, c := range contours {
|
||||||
|
area := gocv.ContourArea(c)
|
||||||
|
if area < minimumMotionArea {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user