diff --git a/README.md b/README.md index 046abc1..a112b32 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,51 @@ # storage-security -Security solution for my storage locker. Deployed to a raspberry pi with an attached camera \ No newline at end of file +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 +``` \ No newline at end of file diff --git a/main.go b/main.go new file mode 100644 index 0000000..5928771 --- /dev/null +++ b/main.go @@ -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 +}