# Bots Readme — Motion Detection (mofin) Pipeline Addition ## Overview Added a motion detection stage (`mofin`) between `runCam` and `terp` in the video surveillance pipeline. ### Old Pipeline ``` runCam ──TCP──▶ terp ──TCP──▶ coordinator ``` ### New Pipeline ``` runCam ──TCP──▶ mofin ──TCP──▶ terp ──TCP──▶ coordinator └──TCP──▶ coordinator (no-motion clips) ``` `mofin` runs pixel-level motion detection on each clip. Clips with motion are forwarded to `terp` for classification (Haar cascade object detection). Clips without motion are sent directly to `coordinator` for storage, bypassing classification. --- ## Files Created ### `frame/motion.go` Contains motion detection types and methods in the `frame` package. **`Comparison` struct:** ```go type Comparison struct { Guid1, Guid2 []byte PixelsChanged int Threshold uint8 } ``` Stores the result of comparing two adjacent frames in a clip — their GUIDs, how many pixels changed, and the pixel-value threshold used. **`(*Frame).compareTo(next *Frame, threshold uint8) (int, error)`:** - Converts both frames to `gocv.Mat` via `ToMat()` - Computes `AbsDiff` between the two Mats - Converts to grayscale if multi-channel (`CvtColor`) - Applies `Threshold` (pixel values above `threshold` count as changed) - Returns `CountNonZero` (number of changed pixels) or `(0, error)` on failure **`(*Clip).CountChangedPixels(threshold uint8)`:** - Iterates over adjacent frame pairs in `c.PixelMats` - For each pair, creates lightweight `Frame` objects from the clip's metadata and calls `compareTo` - Logs any error returned by `compareTo` (avoiding writing to ephemeral Frame copies) - Appends a `Comparison` entry to `c.Comparisons` ### `mofin/main.go` A new pipeline binary that: | Flag | Default | Purpose | |---|---|---| | `-listen` | `:8083` | TCP address to receive clips from `runCam` | | `-terp` | `localhost:8081` | Forward address for clips with motion | | `-coordinator` | `localhost:8082` | Forward address for clips without motion | | `-threshold` | `5000` | Minimum `PixelsChanged` to consider motion present | | `-passphrase` | `""` | Decryption/re-encryption passphrase (mirrors `terp` pattern) | **Concurrency:** Clips are processed in parallel — each incoming clip gets its own goroutine (tracked by `procWg`). On shutdown, the listener is closed, in-flight decode goroutines drain via `acceptWg`, `clipChan` is closed, and `procWg.Wait()` blocks until all processing finishes before exiting. **`processClip` flow:** 1. If `passphrase` is set, decrypt each frame's `PixelBytes` in-place via `Sublimate` + `Decrypt` 2. Call `clip.CountChangedPixels(30)` — pixel-value sensitivity of 30 3. Check if any `Comparison.PixelsChanged >= threshold` flag 4. Re-encrypt if passphrase was set 5. Forward to `terp` address (motion) or `coordinator` address (no motion) ### `mofin/go.mod` Standard module setup with `replace frame => ../frame` (same pattern as `runCam`, `terp`, `coordinator`). --- ## Files Modified ### `frame/frame.go` - Added `Errors []string` field to `Frame` — stores descriptive pipeline errors for backend review - Added `Comparisons []Comparison` field to `Clip` — stores pairwise motion comparison results - Removed a stale duplicate `Detect` method declaration at end of file ### `runCam/main.go` - Renamed `-terp` flag to `-mofin` (default `localhost:8083`) - Updated all internal references from `terpAddr` → `mofinAddr` in `main()`, `rebalance()`, and `runCamera()` ### `config.yaml` Added `mofin1` node between `cam1` and `terp1`: ```yaml - name: "mofin1" binary: "mofin" flags: listen: ":8083" terp: "localhost:8081" coordinator: "localhost:8082" threshold: 5000 ``` Also updated `cam1` to point `mofin: "localhost:8083"` instead of `terp: "localhost:8081"`. --- ## Tests ### `frame/frame_test.go` — 19 tests | Test | What it covers | |---|---| | `TestToMat_Roundtrip` | `ToMat()` returns an identical Mat from Frame data | | `TestToMat_NonNilResult` | `ToMat()` succeeds with valid 2×2 grayscale data | | `TestToMat_EmptyBytes` | `ToMat()` errors on zero-size buffer | | `TestEncryptDecrypt_Roundtrip` | Encrypt then decrypt with same passphrase restores original bytes | | `TestEncryptDecrypt_WrongPassphrase` | Decrypt with wrong passphrase fails | | `TestEncryptDecrypt_EmptyBytes` | Encrypt/Decrypt on nil `PixelBytes` is a no-op | | `TestDecrypt_TooShort` | Decrypt on truncated data fails | | `TestEncrypt_UniqueSaltPerCall` | Two encryptions of same data produce different ciphertexts | | `TestSublimate_Basic` | `Clip.Sublimate()` produces correct Frame count, metadata, GUIDs, timestamps | | `TestSublimate_EmptyClip` | Empty clip produces zero frames | | `TestSublimate_DetectionsCarryOver` | Frame detections are correctly mapped from Clip | | `TestCheckLenCorrelations_Match` | Equal-length slices return true | | `TestCheckLenCorrelations_PixelMatsGuidsMismatch` | Mismatched PixelMats/Guids returns false | | `TestCheckLenCorrelations_TimestampsMismatch` | Mismatched Timestamps returns false | | `TestCheckLenCorrelations_Empty` | Empty clip returns true | | `TestClipSend` | Send/Receive roundtrip over TCP preserves Clip data | | `TestClipSend_InvalidAddress` | Send to unreachable address returns error | | `TestFrameFields_ZeroValues` | Zero-value Frame has nil Errors, nil Detections | | `TestClipFields_ZeroValues` | Zero-value Clip has nil Comparisons | ### `frame/motion_test.go` — 12 tests | Test | What it covers | |---|---| | `TestCompareTo_IdenticalFrames` | Two identical frames → 0 changed pixels | | `TestCompareTo_AllPixelsChanged` | 0→255 → all 16 pixels of 4×4 detected as changed | | `TestCompareTo_PartialChange` | 3 of 16 pixels differing → exactly 3 counted | | `TestCompareTo_ThresholdFiltersSmallDiffs` | Pixels with diff > threshold counted; diff ≤ threshold ignored | | `TestCompareTo_MultiChannel` | Multi-channel (BGR) → grayscale conversion works, changed pixels detected | | `TestCompareTo_DimensionMismatch` | Different sizes return error | | `TestCompareTo_ChannelMismatch` | Different channel counts return error | | `TestCompareTo_IdenticalPixelData` | Same pixel bytes → 0 changed (no crash with equal data) | | `TestCountChangedPixels_Basic` | 3 frames → 2 comparisons, correct pixel counts | | `TestCountChangedPixels_SingleFrame` | 1 frame → 0 comparisons | | `TestCountChangedPixels_EmptyClip` | 0 frames → 0 comparisons | | `TestCountChangedPixels_ResetsComparisons` | Pre-existing Comparisons are cleared before run | | `TestCountChangedPixels_ThresholdParameter` | Pixel-value threshold correctly filters small diffs | | `TestComparison_Fields` | Comparison struct fields store/retrieve correctly | ### `frame/utils_test.go` — 16 tests Camera-dependent tests skip with `-short` flag. Pure-logic tests (empty range, negative index, struct fields, Sscanf) always run. ### Running ```bash go test -short ./... # fast (skips camera hardware scans) go test ./... # full suite (attempts V4L device probes) ``` All 42+ tests pass. Pre-existing `ScanCams` tests preserved and corrected. ## Build Verification All five modules pass `go build ./...` and `go vet ./...`: - `frame/` - `runCam/` - `mofin/` - `terp/` - `coordinator/`