# Project export: Watchdog

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: Cal Hacks 12.0
- Tagline: Watch and protect: WatchdogsEnsure your belongings are protected.
- Devpost: https://devpost.com/software/watchdog-529l7y
- GitHub: https://github.com/ThinkerDesigns/calhacks122025
- Video: https://www.youtube.com/embed/gTqMFqhcuNU?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 0 GitHub contributor(s) — 

## Devpost submission (written by the team)

### Inspiration

During our plane ride to the venue, we ran into someone who had their stuff stolen at the airport. In that moment, we both came had a sudden urge to contruct a system that not only watches over your items with real-time camera processing, but also preventing thiefs from taking your belongings.

### What it does

The premise of the program is to watch over belongings and to actively prevent thieves from stealing items.

### How we built it

We utilized Raspberry Pi 4B to create a portable camera system using a USB Logitech to stream real-time video. In addition to this, we coded YOLO and OpenCV to process and determine belongings. The info is then referenced to a website which allows for the user to activate and send information to the SSH server.

### Challenges we ran into

Essentially, majority of the group had never used ML learning or Raspberry Pi, so a lot of learning on the fly was done. Whether it be consulting mentors for hours, or terrible internet connection which affected our stream bitrate, we managed to conquer every obstacle that we encountered.

### Accomplishments we're proud of

Being able to present the judges a finished product Had a better creation experience than all of our previous hackathons Met life-long connections and friends A severe caffeine addiction

### What we learned

We each learned a little more about ML, Raspberry Pi, the communication between servers, SSH communication, and connecting the Backend with the entire system

### What's next

In the future, we plan to scale our product to be more affordable and also train our own AI model to determine thief facial expressions, suspicious movement patterns and have a more reactable program toward the thief rather than the belongings. Additionally, the effects of the Raspberry Pi are effectively wasted and we plan to integrate a cheaper system to work around Wifi and cost issues.

## README (from the GitHub repository)

## THIS IS A README

## Detected evidence (automated analysis)

Indexed codebase: 5 recognized source files, 27 KB.
- HTML (language) — detected in the code
- Python (language) — detected in the code

## Codebase structure (from repository index)

### Files (7 of 7)

```
custom/custom.html
main.py
README.md
script.py
templates/index.html
yolo11m.pt
yolov8n.pt
```

### Dependencies

No dependency index available.

### Recent commits (newest first)

- final
- Initial commit

## Key source files (fetched from GitHub, selected and truncated for size)

### main.py

```python
import os
import time
import threading
import uuid
import atexit
import ssl
import smtplib
import requests
from datetime import datetime
from email.message import EmailMessage
from flask import Flask, render_template, Response, jsonify, send_file
import cv2
from ultralytics import YOLO

# New dependency for SSH:
import paramiko

# ---------------- CONFIG (env vars) ----------------
MODEL_PATH = os.getenv("MODEL_PATH", "yolo11m.pt")
CAPTURE_DIR = os.getenv("CAPTURE_DIR", "captures")
os.makedirs(CAPTURE_DIR, exist_ok=True)

# Camera & perf
FRAME_WIDTH = int(os.getenv("FRAME_WIDTH", "320"))
FRAME_HEIGHT = int(os.getenv("FRAME_HEIGHT", "240"))
CAMERA_INDEX = int(os.getenv("CAMERA_INDEX", "1"))

# Detection tuning
TARGET_CLASS = os.getenv("TARGET_CLASS", "suitcase").lower()
PERSON_CLASS = os.getenv("PERSON_CLASS", "person").lower()
MOVE_THRESH = float(os.getenv("MOVE_THRESH", "50"))
MISSING_FRAMES_LIMIT = int(os.getenv("MISSING_FRAMES_LIMIT", "15"))
PERSON_CAPTURE_COOLDOWN = float(os.getenv("PERSON_CAPTURE_COOLDOWN", "5.0"))
IMG_QUALITY = int(os.getenv("IMG_QUALITY", "70"))

# Email config (required to actually email)
EMAIL_ALERTS_ENABLED = os.getenv("EMAIL_ALERTS_ENABLED", "true").lower() in ("1", "true", "yes")
SMTP_SERVER = os.getenv("SMTP_SERVER", "smtp.gmail.com")
SMTP_PORT = int(os.getenv("SMTP_PORT", "465"))
SMTP_USER = os.getenv("SMTP_USER", "usb163016@gmail.com")
SMTP_PASSWORD = os.getenv("SMTP_PASSWORD", "xwta yprh zluo gfmm")
EMAIL_FROM = os.getenv("EMAIL_FROM", SMTP_USER)
EMAIL_TO = os.getenv("EMAIL_TO", "bhadauriya@ucdavis.edu")

# Claude (Anthropic) optional config
CLAUDE_API_URL = os.getenv("CLAUDE_API_URL", "")
CLAUDE_API_KEY = os.getenv("CLAUDE_API_KEY", "")

# LiveKit optional config
LIVEKIT_URL = os.getenv("LIVEKIT_URL", "")
LIVEKIT_ENABLED = bool(LIVEKIT_URL)

# Misc
SERVER_BASE_URL = os.getenv("SERVER_BASE_URL", "http://localhost:8010")

# ---------------- SSH CONFIG ----------------
# Remote host to call when suitcase is stolen. Either password auth or key auth supported.
REMOTE_HOST = os.getenv("REMOTE_HOST", "10.42.0.1")            # e.g. "192.168.1.100" or "remote.example.com"
REMOTE_PORT = int(os.getenv("REMOTE_PORT", "22"))
REMOTE_USER = os.getenv("REMOTE_USER", "watchdog")            # e.g. "pi"
REMOTE_PASSWORD = os.getenv("REMOTE_PASSWORD", "123456")    # optional, if using key auth leave blank
REMOTE_KEY_PATH = os.getenv("REMOTE_KEY_PATH", "")    # optional path to private key, e.g. "/home/user/.ssh/id_rsa"
REMOTE_COMMAND = os.getenv("REMOTE_COMMAND", "python3 /home/watchdog/buzzer_test.py")      # command to run remotely, e.g. "bash /home/pi/handle_theft.sh"
SSH_CONNECT_TIMEOUT = float(os.getenv("SSH_CONNECT_TIMEOUT", "10.0"))

# ---------------- Initialize app, camera, model ----------------
app = Flask(__name__)

# single camera capture (AVFoundation for macOS); adjust if on other platform
camera = cv2.VideoCapture("http://10.42.0.1:8080/?action=stream")
#camera = cv2.VideoCapture(CAMERA_INDEX, cv2.CAP_AVFOUNDATION)
camera.set(cv2.CAP_PROP_BUFFERSIZE, 1)
camera.set(cv2.CAP_PROP_FRAME_WIDTH, FRAME_WIDTH)
camera.set(cv2.CAP_PROP_FRAME_HEIGHT, FRAME_HEIGHT)
if not camera.isOpened():
    raise RuntimeError(f"Could not open webcam index {CAMERA_INDEX}; check permissions/index")

# load YOLO model
try:
    model = YOLO(MODEL_PATH)
except Exception as e:
    print("Failed to load model:", e)
    raise

# ---------------- Shared state ----------------
system_armed = False
status = "System Disarmed"
status_lock = threading.Lock()

latest_frame = None
latest_frame_lock = threading.Lock()

last_suitcase_center = None
missing_frames = 0

last_person_capture_time = 0.0
person_photos = {}  # filename -> metadata
person_photos_lock = threading.Lock()

emailed_for_current_stolen = False
emailed_lock = threading.Lock()

# ---------------- Helpers ----------------
def update_status(new_status, frame_for_snapshot=None):
    """
    Update status; on transition to '🔴 Stolen' save snapshot, send email, and optionally run remote SSH command.
    """
    global status, emailed_for_current_stolen
    with status_lock:
        old = status
        if new_status != old:
            status = new_status
            print(f"[STATUS] {old} -> {new_status}")
            # entering stolen
            if new_status == "🔴 Stolen":
                with emailed_lock:
                    if not emailed_for_current_stolen:
                        emailed_for_current_stolen = True
                        # save snapshot and send email in background
                        fpath = None
                        if frame_for_snapshot is not None:
                            fpath = save_stolen_snapshot(frame_for_snapshot)
                        else:
                            with latest_frame_lock:
                                lf = None if latest_frame is None else latest_frame.copy()
                            if lf is not None:
                                fpath = save_stolen_snapshot(lf)
                        if EMAIL_ALERTS_ENABLED and fpath:
                            threading.Thread(target=send_stolen_email, args=(fpath,), daemon=True).start()

                        # Run SSH command (non-blocking)
                        if REMOTE_HOST and REMOTE_USER and REMOTE_COMMAND:
                            threading.Thread(target=run_ssh_command, args=(REMOTE_HOST, REMOTE_PORT, REMOTE_USER, REMOTE_PASSWORD, REMOTE_KEY_PATH, REMOTE_COMMAND), daemon=True).start()
                        else:
                            print("[SSH] Remote SSH not executed: REMOTE_HOST/REMOTE_USER/REMOTE_COMMAND not configured.")
            # leaving stolen
            if old == "🔴 Stolen" and new_status in ("🟢 Moving", "🟡 Standing still"):
                with emailed_lock:
                    emailed_for_current_stolen = False
                delete_photos_kept_due_to_stolen()

def center_of_box(box):
    x1, y1, x2, y2 = box
    return (int((x1 + x2) / 2), int((y1 + y2) / 2))

def save_person_pho
[truncated — 15266 more characters]
```

### script.py

```python
import cv2
for idx in range(4):
    cap = cv2.VideoCapture(idx, cv2.CAP_AVFOUNDATION)
    ok, frame = cap.read()
    print(f"Index {idx}: Opened={cap.isOpened()} FrameOK={ok}")
    cap.release()
```

### custom/custom.html

```html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>WATCHDOG - Custom Feed</title>
  <style>
    body {
      font-family: 'Segoe UI', sans-serif;
      background: #1d1f20;
      color: #e0e0e0;
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      height: 100vh;
      margin: 0;
    }
    h1 {
      color: #80e0ff;
      text-shadow: 0 0 15px #80e0ff;
    }
    video, img {
      width: 50%;
      border-radius: 15px;
      margin-top: 20px;
      box-shadow: 0 4px 10px rgba(0,0,0,0.6);
    }
    input, button {
      padding: 10px;
      border-radius: 8px;
      border: none;
      margin: 5px;
      font-size: 1rem;
    }
    input {
      width: 300px;
    }
    button {
      background: #8055ff;
      color: white;
      cursor: pointer;
    }
    button:hover {
      background: #a070ff;
    }
    #statusBox {
      margin-top: 20px;
      padding: 10px 20px;
      background: rgba(0,0,0,0.7);
      border-radius: 10px;
      font-size: 1.3rem;
      color: #9e9e9e;
    }
  </style>
</head>
<body>
  <h1>WATCHDOG - Custom Camera</h1>

  <div>
    <input type="text" id="streamUrl" placeholder="Enter RTSP or HTTP stream URL">
    <button id="setStreamBtn">Set Stream</button>
  </div>

  <img id="customFeed" src="" alt="Camera Feed" style="display:none;">
  <div id="statusBox">Waiting for stream...</div>

  <div>
    <button id="armBtn">Arm System</button>
    <button id="disarmBtn">Disarm System</button>
  </div>

  <script>
    document.getElementById('setStreamBtn').addEventListener('click', async () => {
      const url = document.getElementById('streamUrl').value;
      if (!url) {
        alert("Please enter a stream URL.");
        return;
      }
      const res = await fetch('/set_stream', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ url })
      });
      const data = await res.json();
      if (res.ok) {
        document.getElementById('customFeed').src = '/custom_feed';
        document.getElementById('customFeed').style.display = 'block';
        document.getElementById('statusBox').textContent = "Connecting...";
      } else {
        alert(data.error || "Failed to set stream.");
      }
    });

    async function updateStatus() {
      const res = await fetch('/status');
      const data = await res.json();
      document.getElementById('statusBox').textContent = data.status;
    }

    setInterval(updateStatus, 1000);

    document.getElementById('armBtn').addEventListener('click', async () => {
      await fetch('/arm', { method: 'POST' });
      updateStatus();
    });

    document.getElementById('disarmBtn').addEventListener('click', async () => {
      await fetch('/disarm', { method: 'POST' });
      updateStatus();
    });
  </script>
</body>
</html>

```

### templates/index.html

```html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>WATCHDOG</title>
  <style>
    /* General styles */
    body {
      font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
      background: #1d1f20; /* Updated background */
      color: #e0e0e0;
      text-align: center;
      margin: 0;
      padding: 0;
      display: flex;
      flex-direction: column;
      justify-content: center;
      align-items: center;
      height: 100vh;
      overflow: hidden;
    }

    h1 {
      font-size: 2.5rem;
      letter-spacing: 2px;
      color: #80e0ff;
      margin: 20px;
      text-transform: uppercase;
      text-shadow: 0 0 10px #80e0ff, 0 0 20px #80e0ff;
    }

    video, img {
      width: 50%;
      border-radius: 15px;
      margin-top: 20px;
      box-shadow: 0 4px 10px rgba(0, 0, 0, 0.6);
      border: 2px solid #333;
      transition: transform 0.3s ease;
    }

    video:hover, img:hover {
      transform: scale(1.05);
    }

    #statusBox {
      margin-top: 20px;
      font-size: 1.6em;
      color: #9e9e9e;
      text-transform: uppercase;
      letter-spacing: 1px;
      padding: 10px 20px;
      background: rgba(0, 0, 0, 0.7);
      border-radius: 12px;
      box-shadow: 0 2px 8px rgba(0, 0, 0, 0.5);
      transition: color 0.3s ease;
    }

    /* Glowing button effect */
    .button {
      text-decoration: none;
      color: rgba(255, 255, 255, 0.8);
      background: rgb(145, 92, 182);
      padding: 15px 40px;
      border-radius: 50px; /* Rounded buttons */
      font-weight: bold;
      text-transform: uppercase;
      letter-spacing: 1px;
      transition: all 0.3s ease-in-out;
      box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3);
    }

    .button:hover {
      color: rgba(255, 255, 255, 1);
      box-shadow: 0 5px 15px rgba(145, 92, 182, .4);
      transform: translateY(-5px);
    }

    .button:active {
      transform: translateY(2px);
    }

    /* Responsive Design */
    @media (max-width: 768px) {
      video, img {
        width: 85%;
      }

      h1 {
        font-size: 2rem;
      }

      #statusBox {
        font-size: 1.2em;
      }

      .button {
        padding: 10px 20px;
        font-size: 1rem;
      }
    }
  </style>
</head>
<body>
  <h1>WATCHDOG</h1>
  <img src="{{ url_for('video_feed') }}" alt="Video Feed">
  <div id="statusBox">Loading status...</div>
  <div>
    <button class="button" id="armBtn">Arm System</button>
    <button class="button" id="disarmBtn">Disarm System</button>
  </div>

  <script>
    async function updateStatus() {
      const res = await fetch('/status');
      const data = await res.json();
      document.getElementById('statusBox').textContent = data.status;
    }

    setInterval(updateStatus, 500);

    document.getElementById('armBtn').addEventListener('click', async () => {
      await fetch('/arm', { method: 'POST' });
      updateStatus();
    });

    document.getElementById('disarmBtn').addEventListener('click', async () => {
      await fetch('/disarm', { method: 'POST' });
      updateStatus();
    });
  </script>
</body>
</html>

```