# Project export: PipeDown

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 2026
- Tagline: Using computer vision to detect damages within pipes and send alerts fast.
- Devpost: https://devpost.com/software/tbd-nprbw3
- GitHub: https://github.com/aditivv/berkai
- Video: https://www.youtube.com/embed/3EoKXOIdzlQ?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 2 GitHub contributor(s) — Shrujan Sriram (7 commits), Aditi Varia (3 commits)

## Devpost submission (written by the team)

### Inspiration

One of our team members recently did a research project on pipe line inspection and learned how immensely expensive mandated pipe inspections are. Each inspection costs $2,500 on average, and a pipeline failure costs approximately $1.4 million in damages. The issue: only about 3-4% of inspected pipes typically actually exhibit damages, meaning that a majority of the $2,500 cost of inspection per pipeline goes to waste. To prevent such a waste of time, resources, and money, we came up with our solution, PipeDown.

### What it does

PipeDown is a tool that can be used to constantly monitor the inside of pipes, detect pipe damages when they occur, and isolate the location at which repairs are necessary. Each of our developed nodes contains a live camera and temperature sensor, and is built to be placed at intervals along the inside of pipes. Users can then log into our software, which is an interactive map that displays all nodes within a certain region, and view the status (safe, warning, or damaged based on temperature and presence of cracks, holes, etc.), live temperature, and live camera footage of each individual node if desired. If any node becomes damaged, the user will instantly receive an email stating the location of the damaged node, which will completely eliminate the unnecessarily repetitive pipe inspections that occur today. Rather than a recurring $2,500 fee, this fee will only have to be paid once for the installation of our system within a pipeline, and damage control only has to be deployed when absolutely necessary.

### How we built it

We used an Arduino to get temperature sensor data and a Raspberry Pi 4 to get live webcam feed as well as to run our entire program. A majority of our code was written in Python, and we used the Browserbase API to create our live email alert system. We used an OpenCV script to detect the presence of cracks, holes, and breaks in the pipe, which we then fed our live camera feed into to accurately detect when damages occur. Finally, we created a box to hold our components together using cardboard and tape, which serves as a very low quality MVP of what our product may look like in the future.

### Challenges we ran into

This was our first time working with hardware at a hackathon, and for some of us, our first time working with hardware ever. With our limited experience, it was initially difficult to work with and understand these hardware components (Arduino and Raspberry Pi). Once we gained some familiarity with the tools, we decided to attempt to use a QNX Raspberry Pi to run our project instead. However, after spending over 5 hours working with the new Pi, we were eventually unable to navigate through the Pi's new OS, as we had no experience working with new operating systems ourselves.

### Accomplishments we're proud of

We are extremely proud that we were able to create a hardware-based project with a very real application even with our lack of hardware experience. We entered the hackathon with little to no knowledge on how to even use a Raspberry Pi, and emerged with a fully functional MVP for a product that could potentially one day become a concept behind a startup.

### What we learned

We learned how to use a Raspberry Pi to run software and gather data through sensors such as webcams, and we learned how to use an Arduino to collect additional sensor data. We also learned how to use OpenCV for real time crack detection

### What's next

To make this product marketable, we plan to first decrease the size of the product (potentially by using ESP32s rather than Raspberry Pi's and looking into finding smaller hardware components to replace those that we used in this project. Our next step would be to look into better materials for the product, ideally water-resistant and tough external materials that can withstand the conditions inside a pipe and protect the hardware inside. Finally, we would potentially like to look into adding a motor to our product so that it could move on its own, making each node movable and therefore able to encompass larger distances and even be applicable beyond pipe inspection. All this while, we hope to continue training a YOLO model for a higher accuracy level in our live damage detection.

## README (from the GitHub repository)

No README available.

## Detected evidence (automated analysis)

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

## Codebase structure (from repository index)

### Files (8 of 8)

```
.gitignore
ai_triage.py
app.py
config.py
pipe-monitoring-handoff.md
route_planner.py
sensor_reader.py
static/index.html
```

### Dependencies

No dependency index available.

### Recent commits (newest first)

- feat: add continuous pipeline backbone under colored segments
- feat: change normal pipeline color from green to blue
- feat: add node detail panel with camera placeholder and force anomaly button
- feat: integrate live API polling with real-time state updates
- feat: split pipeline into state-colored segments with legend
- feat: add MapLibre map with hardcoded pipeline and node markers
- context file
- feat: hard-coded sensor data that displays on localhost:5000 in JSON format
- test code
- feat: created empty files indicated by claude

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

### pipe-monitoring-handoff.md

```markdown
# Distributed Pipe Monitoring Digital Twin — Build Handoff

## Overview

A network of fixed monitoring nodes (camera + temperature sensor) streams data into a software layer that mirrors the pipe network in real time, detects changes, and flags abnormal segments. For the hackathon, we build one physical node and simulate 4–8 more in software so the dashboard shows a realistic distributed deployment.

## Role split

**Software / robotics lead** — node-to-server data flow, digital twin backend, image change detection, dashboards, multi-node simulation, alerting/risk-scoring logic.

**Environmental engineer** — defines which visual patterns matter (buildup, residue, discoloration, bio-growth), sets temperature thresholds and trend logic, shapes severity scoring, frames the real-world relevance for the pitch.

## Hardware stack

We already have a Raspberry Pi, a USB webcam, and an Arduino — no need to buy a separate camera module or microcontroller.

**Decision: Pi as gateway, Arduino as sensor node.**

| Role | Device | Job |
|---|---|---|
| Sensor node | Arduino | Reads the temperature sensor, sends readings over USB serial |
| Gateway | Raspberry Pi | Captures webcam frames, receives Arduino's serial data, posts both to the backend over WiFi |

This mirrors a real sensor-node/gateway architecture and uses everything we already own. It also gives us a fallback: if the Arduino serial link flakes during judging, the Pi can read the temp sensor directly off its own GPIO instead.

**To buy:** one DS18B20 (or DHT22) temperature sensor (~$1–3) and jumper wires. That's the entire hardware budget.

**Wiring:**
- Temp sensor → Arduino digital pin.
- Arduino sketch reads the sensor and prints a JSON or CSV line over Serial (9600 baud).
- Webcam → Pi via USB.

## Software stack

| Layer | Tool | Why |
|---|---|---|
| Node-side capture | `pyserial` (read Arduino) + `opencv-python` (`cv2.VideoCapture`) for webcam frames | Free, minimal setup, runs directly on the Pi |
| Backend | Python + FastAPI | Fast to write, handles ingestion, storage, anomaly logic |
| Storage | SQLite | Zero setup, fine at hackathon scale |
| Anomaly detection | OpenCV (frame differencing / structural similarity) | No trained model needed for the MVP |
| Digital twin layer | Custom Python module on top of the backend | Stores baselines, current state, and active alerts per segment |
| Frontend | React (Vite) + Recharts or Chart.js | Pipe diagram, live snapshots, temperature trends, alerts |
| Simulation layer | Python script generating 4–8 virtual nodes | Synthetic temperature drift + perturbed camera frames, posted to the same ingestion endpoint as the real node |
| Hosting | Local laptop for the demo; free tier on Render/Railway if remote access is needed | No paid infra required |

## Architecture & data flow

1. **Node layer** — Arduino (temp sensor) + Pi (webcam + gateway), plus 4–8 simulated nodes.
2. **Transport layer** — Arduino → Pi over USB serial. Pi → backend over WiFi via HT
[truncated — 2126 more characters]
```

### app.py

```python
from flask import Flask, jsonify, request, send_from_directory
from datetime import datetime

from config import SEGMENTS
from sensor_reader import get_segment_status, start_sensor_thread, force_anomaly

app = Flask(__name__, static_folder='static')

@app.route('/')
def index():
    return send_from_directory('static', 'index.html')

@app.route('/api/twin-state')
def twin_state():
    status = get_segment_status()
    anomalies = [k for k, v in status.items() if v["state"] == "anomaly"]
    warnings = [k for k, v in status.items() if v["state"] == "warning"]
    rising = [k for k, v in status.items() if v["trend"] == "rising"]

    return jsonify({
        "segments": status,
        "anomalies": anomalies,
        "warnings": warnings,
        "rising_trends": rising,
        "timestamp": datetime.now().isoformat()
    })

@app.route('/api/demo-anomaly', methods=['POST'])
def demo_anomaly():
    data = request.json
    segment_id = int(data.get('segment_id', 0))
    force_anomaly(segment_id)
    return jsonify({"ok": True, "segment_id": segment_id})

if __name__ == '__main__':
    start_sensor_thread(use_arduino=False)  # flip to True once Arduino is wired
    app.run(host='0.0.0.0', port=5000, debug=True)
```

### config.py

```python
SEGMENTS = {
    0: {"name": "Segment A — Near Intake",  "x": 100, "y": 200, "temp_normal": (60, 75)},
    1: {"name": "Segment B — Mid Run",      "x": 300, "y": 200, "temp_normal": (60, 75)},
    2: {"name": "Segment C — Near Outflow", "x": 500, "y": 200, "temp_normal": (60, 75)},
}
```

### sensor_reader.py

```python
import threading
import random
import time
import serial
from datetime import datetime
from config import SEGMENTS

segment_readings = {}
reading_history = {}
lock = threading.RLock()

# ── SIMULATED MODE ──────────────────────────────────────

def simulate_sensors():
    with lock:
        for seg_id, seg in SEGMENTS.items():
            mid = (seg["temp_normal"][0] + seg["temp_normal"][1]) / 2
            segment_readings[seg_id] = {"temp": mid, "last_seen": datetime.now().isoformat()}

    while True:
        with lock:
            for seg_id in SEGMENTS:
                current = segment_readings[seg_id]["temp"]
                drift = random.uniform(-0.5, 0.5)
                new_temp = current + drift
                record_segment_reading(seg_id, new_temp)
        time.sleep(3)

# ── REAL ARDUINO MODE (use this once hardware is wired) ──

def read_arduino(port='/dev/ttyACM0'):
    try:
        ser = serial.Serial(port, 9600, timeout=2)
        print(f"Arduino connected on {port}")
    except Exception as e:
        print(f"Arduino not found ({e}) — falling back to simulation")
        simulate_sensors()
        return

    while True:
        try:
            line = ser.readline().decode('utf-8').strip()
            if line.startswith("SEGMENT:"):
                parts = line.split(":")
                segment_id = int(parts[1])
                temp = float(parts[2])
                record_segment_reading(segment_id, temp)
        except Exception as e:
            print(f"Read error: {e}")

# ── SHARED LOGIC ─────────────────────────────────────────

def record_segment_reading(segment_id, temp):
    with lock:
        now = datetime.now().isoformat()
        segment_readings[segment_id] = {"temp": temp, "last_seen": now}
        reading_history.setdefault(segment_id, []).append({"temp": temp, "time": now})
        reading_history[segment_id] = reading_history[segment_id][-50:]

def get_trend(segment_id):
    history = reading_history.get(segment_id, [])
    if len(history) < 5:
        return "insufficient_data"
    recent = [h["temp"] for h in history[-5:]]
    older = [h["temp"] for h in history[:-5]] or recent
    recent_avg = sum(recent) / len(recent)
    older_avg = sum(older) / len(older)
    if recent_avg - older_avg > 2:
        return "rising"
    elif older_avg - recent_avg > 2:
        return "falling"
    return "stable"

def get_segment_status():
    status = {}
    with lock:
        for seg_id, seg in SEGMENTS.items():
            reading = segment_readings.get(seg_id)
            temp = reading["temp"] if reading else None

            if temp is None:
                state = "unknown"
            elif temp > seg["temp_normal"][1]:
                state = "anomaly"
            elif temp > seg["temp_normal"][1] - 3:
                state = "warning"
            else:
                state = "normal"

            status[seg_id] = {
                **seg,
                "temp": round(temp, 1) if temp else None,
                "state": state,
                "trend": get_trend(seg_id),
                "last_seen": reading["last_seen"] if reading else None,
            }
    return status

def force_anomaly(segment_id):
    """Manually spike a segment's temperature — for testing/demo"""
    with lock:
        spike = SEGMENTS[segment_id]["temp_normal"][1] + 15
        record_segment_reading(segment_id, spike)

def start_sensor_thread(use_arduino=False):
    target = read_arduino if use_arduino else simulate_sensors
    t = threading.Thread(target=target, daemon=True)
    t.start()
```

### static/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>Pipe Digital Twin</title>
  <link rel="stylesheet" href="https://unpkg.com/maplibre-gl@4.7.1/dist/maplibre-gl.css" />
  <style>
    * { margin: 0; padding: 0; box-sizing: border-box; }
    html, body { height: 100%; overflow: hidden; }

    body {
      font-family: 'Segoe UI', Tahoma, sans-serif;
      background: #0f1117;
      color: #e0e0e0;
      display: flex;
      flex-direction: column;
    }

    /* ── Header ── */
    #header {
      background: #1a1d27;
      padding: 10px 20px;
      border-bottom: 1px solid #2a2d3e;
      display: flex;
      align-items: center;
      gap: 12px;
      flex-shrink: 0;
      z-index: 10;
    }
    #header h1 { font-size: 17px; font-weight: 600; color: #fff; flex: 1; }

    /* ── Status bar ── */
    #statusbar {
      background: #131620;
      border-bottom: 1px solid #2a2d3e;
      padding: 6px 20px;
      display: flex;
      align-items: center;
      gap: 20px;
      font-size: 12px;
      color: #888;
      flex-shrink: 0;
    }
    .status-chip {
      display: inline-flex;
      align-items: center;
      gap: 6px;
      padding: 2px 10px;
      border-radius: 12px;
      font-weight: 600;
      font-size: 12px;
    }
    .chip-ok      { background: rgba(33,150,243,.15);  color: #2196f3; }
    .chip-warn    { background: rgba(255,152,0,.15);  color: #ff9800; }
    .chip-anomaly { background: rgba(244,67,54,.15);  color: #f44336; }
    .chip-anomaly.active { background: rgba(244,67,54,.3); animation: pulse-chip 1.2s infinite; }
    @keyframes pulse-chip { 0%,100%{ opacity:1 } 50%{ opacity:.5 } }
    #last-update { margin-left: auto; color: #555; }

    /* ── Content row (map + panel) ── */
    #content { flex: 1; display: flex; overflow: hidden; }

    /* ── Map ── */
    #map-wrap { flex: 1; position: relative; }
    #map { width: 100%; height: 100%; }

    /* ── Legend ── */
    #legend {
      position: absolute;
      bottom: 36px;
      right: 12px;
      background: rgba(26,29,39,.92);
      border: 1px solid #2a2d3e;
      border-radius: 8px;
      padding: 10px 14px;
      font-size: 12px;
      z-index: 5;
      pointer-events: none;
      backdrop-filter: blur(4px);
    }
    #legend h3 { font-size: 11px; text-transform: uppercase; letter-spacing: .8px; color: #666; margin-bottom: 8px; }
    .legend-row { display: flex; align-items: center; gap: 8px; margin-bottom: 5px; }
    .legend-row:last-child { margin-bottom: 0; }
    .legend-dot  { width: 12px; height: 12px; border-radius: 50%; border: 2px solid rgba(255,255,255,.4); flex-shrink: 0; }
    .legend-line { width: 24px; height: 4px; border-radius: 2px; flex-shrink: 0; }

    /* ── Detail panel ── */
    #panel {
      width: 300px;
      flex-shrink: 0;
      background: #1a1d27;
      border-left: 1px solid #2a2d3e;
      display: flex;
      flex-direction: column;
      overflow-y: auto;
      transform: translateX(100%);
      transition: transform .25s ease;
    }
    #panel.open { transform: translateX(0); }

    .panel-header {
      padding: 14px 16px 10px;
      border-bottom: 1px solid #2a2d3e;
      display: flex;
      align-items: flex-start;
      gap: 10px;
    }
    .panel-title { flex: 1; }
    .panel-title h2 { font-size: 14px; font-weight: 600; color: #fff; line-height: 1.4; }
    .panel-title small { font-size: 11px; color: #666; }

    #panel-close {
      background: none;
      border: none;
      color: #666;
      font-size: 20px;
      cursor: pointer;
      line-height: 1;
      padding: 0 2px;
      flex-shrink: 0;
    }
    #panel-close:hover { color: #ccc; }

    /* State badge */
    .state-badge {
      display: inline-block;
      padding: 3px 10px;
      border-radius: 12px;
      font-size: 11px;
      font-weight: 700;
      letter-spacing: .5px;
      text-transform: uppercase;
    }

    /* Camera feed placeholder */
    #camera-wrap {
      margin: 14px 16px 0;
    }
    #camera-wrap label { font-size: 11px; text-transform: uppercase; letter-spacing: .7px; color: #555; display: block; margin-bottom: 6px; }
    #camera-feed {
      background: #0a0c14;
      border: 1.5px dashed #2a2d3e;
      border-radius: 6px;
      height: 170px;
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      gap: 8px;
      color: #444;
      font-size: 12px;
    }
    #camera-feed svg { opacity: .4; }

    /* Stats grid */
    .stat-grid {
      display: grid;
      grid-template-columns: 1fr 1fr;
      gap: 8px;
      margin: 14px 16px 0;
    }
    .stat-box {
      background: #131620;
      border: 1px solid #2a2d3e;
      border-radius: 6px;
      padding: 10px 12px;
    }
    .stat-box .label { font-size: 10px; text-transform: uppercase; letter-spacing: .6px; color: #555; margin-bottom: 4px; }
    .stat-box .value { font-size: 20px; font-weight: 700; color: #fff; }
    .stat-box .unit  { font-size: 11px; color: #666; }

    /* Trend indicator */
    #trend-row {
      margin: 12px 16px 0;
      font-size: 13px;
      color: #888;
    }
    #trend-row b { color: #e0e0e0; }

    /* Divider */
    .divider { height: 1px; background: #2a2d3e; margin: 14px 0; }

    /* Force anomaly button */
    #btn-anomaly {
      margin: 0 16px 16px;
      padding: 9px 0;
      width: calc(100% - 32px);
      background: rgba(244,67,54,.12);
      border: 1px solid rgba(244,67,54,.4);
      border-radius: 6px;
      color: #f44336;
      font-size: 13px;
      font-weight: 600;
      cursor: pointer;
      transition: background .15s;
    }
    #btn-anomaly:hover { background: rgba(244,67,54,.22); }
    #btn-anomaly:disabled { opacity: .4; cursor: not-allowed; }

    #panel-hint {
      position: absolute;
      bottom: 40px;
      left: 50%;
      transform: translateX(-50%);
      background: rgba(26,29,39,.88);
      border: 1px solid #2a2d3e;
      border-
[truncated — 13857 more characters]
```