# Project export: PetPal

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: UC Berkeley AI Hackathon 2025
- Tagline: PetPal is a real-time lost/found pet detection system powered by Raspberry Pi, Coral TPU, and Supabase. It captures and sends labeled media to the cloud for review in a user-friendly web app.
- Devpost: https://devpost.com/software/raspberrypet
- GitHub: https://github.com/annasehgal/calhacks-ai-hackathon
- Demo: https://github.com/riley-1995/raspberry-pet
- Team: 3 GitHub contributor(s) — Ultramas (17 commits), riley-1995 (6 commits), annasehgal (2 commits)

## Devpost submission (written by the team)

### Inspiration

We were inspired by the emotional impact of losing a pet and wanted to create a tech-powered solution to help reunite animals with their families. Combining edge AI with remote sensing felt like the perfect fit for this challenge.

### What it does

Raspberry Pet detects cats and dogs in real-time using a Raspberry Pi and Coral TPU. When a pet is spotted, it captures images and a short video clip, uploads them to Supabase Storage, and logs the detection for review in a web app — all without human supervision.

### How we built it

We retrained and tuned a TensorFlow Lite object classification model (MobileNetV2) on a dataset of labeled pet images. The model runs on the Coral Edge TPU for low-latency inference. A motion detection pipeline using OpenCV on the Pi triggers the camera, processes detections, and uploads media and metadata to Supabase. The frontend is built with Flask, and detections appear in a live dashboard.

### What's next

for RaspberryPet

## README (from the GitHub repository)

# 🐾 Pet Finder – Pi Camera + Coral TPU + Flask Stream

Real-time object detection using a Raspberry Pi with a Pi Camera Module V2 and Coral USB Accelerator. Streams video over Flask. Training and model prep is done on a laptop, and inference runs on the Pi.

---

## 🔧 Raspberry Pi Setup (Bookworm 64-bit)

### 1. System packages

```bash
sudo apt update && sudo apt install -y \
python3-picamera2 v4l-utils libusb-1.0-0-dev libedgetpu1-std \
make build-essential libssl-dev zlib1g-dev libbz2-dev libreadline-dev \
libsqlite3-dev wget curl llvm libncurses5-dev libncursesw5-dev \
xz-utils tk-dev libffi-dev liblzma-dev git
```

### 2. Install pyenv

```bash
curl https://pyenv.run | bash
```

Then add to your ~/.bashrc:

```bash
export PATH="$HOME/.pyenv/bin:$PATH"
eval "$(pyenv init --path)"
eval "$(pyenv virtualenv-init -)"
```

Restart your shell:

```bash
exec "$SHELL"
```

---

### 3. Python 3.9 + virtualenv

```bash
pyenv install 3.9.18
python3.9 -m venv ~/coral-venv --system-site-packages
source ~/coral-venv/bin/activate
```

---

### 4. Install Python packages

```bash
pip install -r requirements.txt
pip install --extra-index-url https://google-coral.github.io/py-repo/ tflite-runtime
pip install https://github.com/google-coral/pycoral/releases/download/v2.0.0/pycoral-2.0.0-cp39-cp39-linux_aarch64.whl
```

---

### 5. Run the stream

```bash
python stream.py
```

View at:  
`http://<your-pi-ip>:5000/video_feed`

---

## 💻 Laptop Setup (Training + Model Prep)

### 1. Python env for training

```bash
pyenv install 3.9.18
'''

#### Create a virtual environment using Python 3.9
```bash
python3.9 -m venv coral-train
source coral-train/bin/activate
```
#### Alternative (if you prefer pyenv-virtualenv)
```bash
pyenv virtualenv 3.9.18 coral-train
pyenv activate coral-train
'''

#### Install training dependencies
```bash
pip install tensorflow-macos==2.13.0 numpy pandas opencv-python
```

### 2. (After building/tuning your model): Convert + compile model

```bash
tflite_convert --output_file=model.tflite ...
edgetpu_compiler model.tflite
```

### 3. Copy model to Pi

```bash
scp model_edgetpu.tflite pi@<pi-ip>:~/models/
```

---

## 🧠 Notes

- Uses `picamera2` for fast streaming
- `libedgetpu1-std` is installed (not max)
- Tested on Raspberry Pi OS Bookworm 64-bit + Coral USB Accelerator


## Detected evidence (automated analysis)

Indexed codebase: 26 recognized source files, 68 KB.
- Flask (technology) — detected in the code
- HTML (language) — detected in the code
- JavaScript (language) — detected in the code
- Python (language) — detected in the code
- React (technology) — detected in the code
- TypeScript (language) — detected in the code
- Supabase (technology) — claimed on Devpost, not found in the code
- TensorFlow (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (35 of 35)

```
__init__.py
.gitignore
app/__init__.py
app/app.py
app/config.py
app/forms.py
app/models.py
app/routes.py
app/services.py
app/supabase_client.py
app/templates/index.html
app/templates/login.html
app/templates/signup.html
instance/app.db
LICENSE
main.py
my-app/.gitignore
my-app/app.json
my-app/app/_layout.tsx
my-app/app/(tabs)/_layout.tsx
my-app/app/(tabs)/found.tsx
my-app/app/(tabs)/index.tsx
my-app/app/(tabs)/lost.tsx
my-app/app/(tabs)/messages.tsx
my-app/app/(tabs)/profile.tex
my-app/app/chat/[id].tsx
my-app/app/components/PetCard.tsx
my-app/app/components/PetForm.tsx
my-app/app/components/ProfileHeader.tsx
my-app/app/types/index.tsx
my-app/eslint.config.js
my-app/package.json
my-app/tsconfig.json
README.md
requirements.txt
```

### Dependencies

- my-app/package.json: @babel/core@^7.25.2, @expo/vector-icons@^14.1.0, @react-navigation/bottom-tabs@^7.3.10, @react-navigation/elements@^2.3.8, @react-navigation/native@^7.1.6, @types/react@~19.0.10, eslint@^9.25.0, eslint-config-expo@~9.2.0, expo@~53.0.12, expo-audio@~0.4.6, expo-av@~15.1.6, expo-blur@~14.1.5, expo-constants@~17.1.6, expo-font@~13.3.1, expo-haptics@~14.1.4, expo-image@~2.3.0, expo-image-picker@~16.1.4, expo-linking@~7.1.5, expo-router@~5.1.0, expo-splash-screen@~0.30.9, expo-status-bar@~2.2.3, expo-symbols@~0.4.5, expo-system-ui@~5.0.9, expo-video@~2.2.2, expo-web-browser@~14.2.0, react@19.0.0, react-dom@19.0.0, react-native@0.79.4, react-native-gesture-handler@~2.24.0, react-native-reanimated@~3.17.4, react-native-safe-area-context@5.4.0, react-native-screens@~4.11.1, react-native-web@~0.20.0, react-native-webview@13.13.5, typescript@~5.8.3
- requirements.txt: Flask, Flask-Migrate, Flask-SQLAlchemy

### Recent commits (newest first)

- Update README.md
- changed flask services
- Merge branch 'main' of https://github.com/annasehgal/calhacks-ai-hackathon
- changed flask gitignore
- Backend flask integration
- Delete app/.env
- Merge branch 'main' into backend
- updated previous commentary
- updated previous commentary
- updated previous commentary
- updated previous commentary
- updated previous commentary
- updated previous commentary
- Merge pull request #3 from annasehgal/backend
- updated previous commentary
- Merge pull request #2 from annasehgal/backend
- updated previous commentary
- updated previous commentary
- updated previous commentary
- changed names to alt_text

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

### requirements.txt

```
Flask
Flask-SQLAlchemy
Flask-Migrate

```

### my-app/package.json

```
{
  "name": "my-app",
  "main": "expo-router/entry",
  "version": "1.0.0",
  "scripts": {
    "start": "expo start",
    "reset-project": "node ./scripts/reset-project.js",
    "android": "expo run:android",
    "ios": "expo run:ios",
    "web": "expo start --web",
    "lint": "expo lint"
  },
  "dependencies": {
    "@expo/vector-icons": "^14.1.0",
    "@react-navigation/bottom-tabs": "^7.3.10",
    "@react-navigation/elements": "^2.3.8",
    "@react-navigation/native": "^7.1.6",
    "expo": "~53.0.12",
    "expo-blur": "~14.1.5",
    "expo-constants": "~17.1.6",
    "expo-font": "~13.3.1",
    "expo-haptics": "~14.1.4",
    "expo-image": "~2.3.0",
    "expo-linking": "~7.1.5",
    "expo-router": "~5.1.0",
    "expo-splash-screen": "~0.30.9",
    "expo-status-bar": "~2.2.3",
    "expo-symbols": "~0.4.5",
    "expo-system-ui": "~5.0.9",
    "expo-web-browser": "~14.2.0",
    "react": "19.0.0",
    "react-dom": "19.0.0",
    "react-native": "0.79.4",
    "react-native-gesture-handler": "~2.24.0",
    "react-native-reanimated": "~3.17.4",
    "react-native-safe-area-context": "5.4.0",
    "react-native-screens": "~4.11.1",
    "react-native-web": "~0.20.0",
    "react-native-webview": "13.13.5",
    "expo-av": "~15.1.6",
    "expo-image-picker": "~16.1.4",
    "expo-video": "~2.2.2",
    "expo-audio": "~0.4.6"
  },
  "devDependencies": {
    "@babel/core": "^7.25.2",
    "@types/react": "~19.0.10",
    "typescript": "~5.8.3",
    "eslint": "^9.25.0",
    "eslint-config-expo": "~9.2.0"
  },
  "private": true
}

```

### main.py

```python

from app import create_app, db

app = create_app()

with app.app_context():
    db.create_all()  # <- This creates tables based on all models

if __name__ == '__main__':
    app.run(debug=True, port=5000)


```

### app/app.py

```python
from flask import Flask

app = Flask(__name__)

@app.route('/')
def home():
    return "Hello, Flask!"

if __name__ == '__main__':
    app.run(debug=True)

```

### my-app/app/_layout.tsx

```typescript
import { Stack } from 'expo-router'
import { SafeAreaProvider } from 'react-native-safe-area-context'

export default function RootLayout() {
  return (
    <SafeAreaProvider>
      <Stack>
        <Stack.Screen name="(tabs)" options={{ headerShown: false }} />
        <Stack.Screen name="chat/[id]" options={{ 
          headerShown: true, 
          title: 'Chat',
          presentation: 'modal'
        }} />
      </Stack>
    </SafeAreaProvider>
  )
}
```

### my-app/app/(tabs)/_layout.tsx

```typescript
import FontAwesome from '@expo/vector-icons/FontAwesome'
import { Tabs } from 'expo-router'
import { StatusBar } from 'expo-status-bar'

export default function TabLayout() {
  return (
    <>
      <StatusBar style="auto" />
      <Tabs 
        screenOptions={{ 
          tabBarActiveTintColor: '#FF6B6B',
          tabBarInactiveTintColor: '#888',
          tabBarStyle: {
            backgroundColor: '#fff',
            borderTopColor: '#eee',
            paddingBottom: 5,
            height: 85
          }
        }}
      >
        <Tabs.Screen
          name="index"
          options={{
            title: 'Home',
            headerShown: false,
            tabBarIcon: ({ color, size }) => <FontAwesome name="home" size={size} color={color} />,
          }}
        />
        <Tabs.Screen
          name="lost"
          options={{
            title: 'Lost',
            headerShown: false,
            tabBarIcon: ({ color, size }) => <FontAwesome name="search" size={size} color={color} />,
          }}
        />
        <Tabs.Screen
          name="found"
          options={{
            title: 'Spotted Pets',
            headerShown: false,
            tabBarIcon: ({ color, size }) => <FontAwesome name="camera" size={size} color={color} />,
          }}
        />
        <Tabs.Screen
          name="messages"
          options={{
            title: 'Messages',
            headerShown: false,
            tabBarIcon: ({ color, size }) => <FontAwesome name="envelope" size={size} color={color} />,
          }}
        />
        <Tabs.Screen
          name="profile"
          options={{
            title: 'Profile',
            headerShown: false,
            tabBarIcon: ({ color, size }) => <FontAwesome name="user" size={size} color={color} />,
          }}
        />
      </Tabs>
    </>
  )
}
```

### my-app/app/types/index.tsx

```typescript
// Pet related types
export type Pet = {
  id: string
  name?: string
  species: string
  breed?: string
  color: string
  description: string
  images: string[]
  contactInfo: string
  location: string
  date: string
  type: 'lost' | 'found'
  reward?: string
  status: 'active' | 'reunited' | 'closed'
}

export type LostPet = Pet & {
  type: 'lost'
  name: string
  lastSeen: string
}

export type FoundPet = Pet & {
  type: 'found'
  foundDate: string
  videoUrl?: string
}

// User and profile types
export type UserProfile = {
  id: string
  name: string
  email: string
  phone: string
  location: string
  avatar: string
  petsReported: number
  petsFound: number
  successfulReunions: number
  joinDate: string
  isVerified: boolean
}

// Message and chat types
export type Message = {
  id: string
  text: string
  senderId: string
  receiverId: string
  timestamp: string
  isRead: boolean
  messageType: 'text' | 'image' | 'location'
  imageUrl?: string
  location?: {
    latitude: number
    longitude: number
    address?: string
  }
}

export type Chat = {
  id: string
  participants: string[]
  lastMessage: Message
  unreadCount: number
  petId?: string
  createdAt: string
  updatedAt: string
}

export type DMItem = {
  id: string
  name: string
  lastMessage: string
  timestamp: string
  unreadCount?: number
  avatar: string
  petType: string
}

// Activity and dashboard types
export type RecentActivity = {
  id: string
  type: 'lost' | 'found' | 'reunited'
  petName: string
  location: string
  timestamp: string
  image: string
  userId: string
}

export type DashboardStats = {
  totalLost: number
  totalFound: number
  totalReunited: number
  myReports: number
  myMessages: number
}

// Form data types
export type PetFormData = {
  name?: string
  species: string
  breed?: string
  color: string
  location: string
  date: string
  description: string
  contactInfo: string
  reward?: string
  images: string[]
}

// Search and filter types
export type SearchFilters = {
  species?: string
  breed?: string
  color?: string
  location?: string
  dateRange?: {
    start: string
    end: string
  }
  hasReward?: boolean
  type: 'lost' | 'found' | 'all'
}

export type SearchResult = Pet & {
  distance?: number
  matchScore?: number
}

// Notification types
export type NotificationType = 
  | 'new_message'
  | 'pet_match'
  | 'pet_found'
  | 'reunion_confirmed'
  | 'system_update'

export type Notification = {
  id: string
  type: NotificationType
  title: string
  message: string
  timestamp: string
  isRead: boolean
  data?: {
    petId?: string
    chatId?: string
    userId?: string
    [key: string]: any
  }
}

// Location types
export type Location = {
  latitude: number
  longitude: number
  address?: string
  city?: string
  state?: string
  country?: string
}

// API response types
export type ApiResponse<T> = {
  success: boolean
  data?: T
  message?: string
  error?: string
}

export type PaginatedResponse<T> = ApiResponse<{
  items: T[]
  totalCount: number
  page: number
  limit: number
  hasMore: boolean
}>

// Navigation types (for type-safe navigation)
export type TabParamList = {
  index: undefined
  lost: undefined
  found: undefined
  messages: undefined
  profile: undefined
}

export type RootStackParamList = {
  '(tabs)': undefined
  'chat/[id]': { id: string }
  'pet/[id]': { id: string }
  'search': { filters?: SearchFilters }
  'notifications': undefined
}

// Component prop types
export type PetCardProps = {
  pet: Pet
  onPress: (pet: Pet) => void
  showType?: boolean
}

export type ProfileHeaderProps = {
  name: string
  email: string
  avatar: string
  onEditPress: () => void
}

export type PetFormProps = {
  type: 'lost' | 'found'
  initialData?: Partial<PetFormData>
  onClose: () => void
  onSubmit: (data: PetFormData) => void
}

// Error types
export type AppError = {
  code: string
  message: string
  details?: any
}

// Theme and styling types
export type Theme = {
  colors: {
    primary: string
    secondary: string
    success: string
    warning: string
    error: string
    background: string
    surface: string
    text: string
    textSecondary: string
    border: string
  }
  spacing: {
    xs: number
    sm: number
    md: number
    lg: number
    xl: number
  }
  borderRadius: {
    sm: number
    md: number
    lg: number
    full: number
  }
}
```

### my-app/app/(tabs)/index.tsx

```typescript
import React, { useState } from 'react'
import { View, Text, StyleSheet, ScrollView, TouchableOpacity, Modal } from 'react-native'
import { useRouter } from 'expo-router'
import PetForm from '../components/PetForm'
import PetCard from '../components/PetCard'
import type { Pet, RecentActivity, DashboardStats, PetFormData } from '../types'

// Mock data for demonstration
const mockStats: DashboardStats = {
  totalLost: 127,
  totalFound: 89,
  totalReunited: 45,
  myReports: 3,
  myMessages: 7
}

const mockRecentActivity: RecentActivity[] = [
  {
    id: '1',
    type: 'lost',
    petName: 'Buddy',
    location: 'Central Park, NYC',
    timestamp: '2h ago',
    image: 'https://images.unsplash.com/photo-1552053831-71594a27632d?w=300',
    userId: 'user1'
  },
  {
    id: '2',
    type: 'found',
    petName: 'Unknown Cat',
    location: 'Brooklyn Bridge',
    timestamp: '4h ago',
    image: 'https://images.unsplash.com/photo-1514888286974-6c03e2ca1dba?w=300',
    userId: 'user2'
  },
  {
    id: '3',
    type: 'reunited',
    petName: 'Luna',
    location: 'Queens',
    timestamp: '1d ago',
    image: 'https://images.unsplash.com/photo-1583337130417-3346a1be7dee?w=300',
    userId: 'user3'
  }
]

const mockPets: Pet[] = [
  {
    id: '1',
    name: 'Max',
    species: 'Dog',
    breed: 'Golden Retriever',
    color: 'Golden',
    description: 'Friendly golden retriever, wearing a blue collar',
    images: ['https://images.unsplash.com/photo-1552053831-71594a27632d?w=300'],
    contactInfo: 'john@email.com',
    location: 'Central Park, NYC',
    date: '2024-06-20',
    type: 'lost',
    reward: '$200',
    status: 'active'
  },
  {
    id: '2',
    species: 'Cat',
    breed: 'Tabby',
    color: 'Orange and white',
    description: 'Found wandering near the subway station',
    images: ['https://images.unsplash.com/photo-1514888286974-6c03e2ca1dba?w=300'],
    contactInfo: 'jane@email.com',
    location: 'Brooklyn Bridge',
    date: '2024-06-21',
    type: 'found',
    status: 'active'
  }
]

export default function HomeScreen() {
  const router = useRouter()
  const [showReportModal, setShowReportModal] = useState(false)
  const [reportType, setReportType] = useState<'lost' | 'found'>('lost')

  const handleReportLost = () => {
    setReportType('lost')
    setShowReportModal(true)
  }

  const handleReportFound = () => {
    setReportType('found')
    setShowReportModal(true)
  }

  const handleFormSubmit = (data: PetFormData) => {
    // Handle form submission here
    console.log('Form submitted:', data)
    setShowReportModal(false)
    // You would typically send this data to your backend
  }

  const handlePetPress = (pet: Pet) => {
    // Corrected navigation path to match the defined route for chat/[id]
    router.push(`/chat/${pet.id}`)
  }

  const handleViewAllLost = () => {
    router.push('/(tabs)/lost')
  }

  const handleViewAllFound = () => {
    router.push('/(tabs)/found')
  }

  const renderActivityIcon = (type: string) => {
    switch (type) {
      case 'lost': return '😢'
      case 'found': return '🎉'
      case 'reunited': return '❤️'
      default: return '📱'
    }
  }

  return (
    <View style={styles.container}>
      <ScrollView style={styles.scrollView} showsVerticalScrollIndicator={false}>
        {/* Header */}
        <View style={styles.header}>
          <Text style={styles.title}>Pet Finder</Text>
          <Text style={styles.subtitle}>Helping reunite pets with their families</Text>
        </View>

        {/* Quick Actions */}
        <View style={styles.quickActions}>
          <TouchableOpacity style={[styles.actionButton, styles.lostButton]} onPress={handleReportLost}>
            <Text style={styles.actionButtonText}>😢 Report Lost Pet</Text>
          </TouchableOpacity>
          <TouchableOpacity style={[styles.actionButton, styles.foundButton]} onPress={handleReportFound}>
            <Text style={styles.actionButtonText}>🎉 Report Found Pet</Text>
          </TouchableOpacity>
        </View>

        {/* Stats Cards */}
        <View style={styles.statsContainer}>
          <View style={styles.statCard}>
            <Text style={styles.statNumber}>{mockStats.totalLost}</Text>
            <Text style={styles.statLabel}>Lost Pets</Text>
          </View>
          <View style={styles.statCard}>
            <Text style={styles.statNumber}>{mockStats.totalFound}</Text>
            <Text style={styles.statLabel}>Found Pets</Text>
          </View>
          <View style={styles.statCard}>
            <Text style={[styles.statNumber, styles.successNumber]}>{mockStats.totalReunited}</Text>
            <Text style={styles.statLabel}>Reunited</Text>
          </View>
        </View>

        {/* Recent Activity */}
        <View style={styles.section}>
          <Text style={styles.sectionTitle}>Recent Activity</Text>
          {mockRecentActivity.map(activity => (
            <View key={activity.id} style={styles.activityItem}>
              <Text style={styles.activityIcon}>{renderActivityIcon(activity.type)}</Text>
              <View style={styles.activityContent}>
                <Text style={styles.activityText}>
                  <Text style={styles.activityType}>{activity.type.toUpperCase()}</Text>: {activity.petName}
                </Text>
                <Text style={styles.activityLocation}>📍 {activity.location}</Text>
              </View>
              <Text style={styles.activityTime}>{activity.timestamp}</Text>
            </View>
          ))}
        </View>

        {/* Recent Lost Pets */}
        <View style={styles.section}>
          <View style={styles.sectionHeader}>
            <Text style={styles.sectionTitle}>Recent Lost Pets</Text>
            <TouchableOpacity onPress={handleViewAllLost}>
              <Text style={styles.viewAllText}>View All</Text>
            </TouchableOpacity>
          </View>
          {mockPets
            .filter(pet => pet.type === 'lost')
            .slice(0, 2)
            .map(pet => (
   
[truncated — 4312 more characters]
```

### app/config.py

```python
import os

class Config:
    SECRET_KEY = os.environ.get('SECRET_KEY') or 'super-secret-key'
    SQLALCHEMY_DATABASE_URI = 'sqlite:///app.db'
    SQLALCHEMY_TRACK_MODIFICATIONS = False


```

### app/supabase_client.py

```python
from supabase import create_client
import os
from dotenv import load_dotenv

load_dotenv()

supabase_url = os.getenv("SUPABASE_URL")
supabase_key = os.getenv("SUPABASE_KEY")
supabase = create_client(supabase_url, supabase_key)

```

[16 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]