Skip to content

Repository files navigation

Buoy Tracker

A real-time web interface for tracking Meshtastic mesh network nodes on a live map.

Live Demo: https://buoy-tracker.sequoiayc.org

The Problem

Racing buoys anchored in tidal waters face a critical risk: mooring chains wear out and break, causing buoys to break free and drift with the tide and wind. Once adrift, expensive buoys are difficult to recover—and AirTags on the buoys only work within Bluetooth range.

To solve this, buoys are equipped with Meshtastic LoRa nodes that transmit GPS positions via mesh network, with position packets received by gateway nodes and relayed through MQTT.

System Architecture

Buoy Tracker System Architecture

Buoy Tracker enables:

  • Real-time tracking: Map-based visualization of buoy positions as updates arrive
  • Drift detection: Automatic alerts when buoys move beyond expected anchoring zones
  • Instant notifications: Email alerts on drift detection
  • Battery monitoring: voltage tracking, low-battery alerts, per-buoy history
  • Mesh-range coverage: Works across miles of mesh network, not limited to Bluetooth

Data flow: Buoys → Mesh Network → Gateways → MQTT Broker → Backend → Dashboard → Alerts

A Note on This Code

This project is 100% vibe coded — an exercise in exploring what Anthropic Claude can build when given a real-world problem and free rein. According to the human involved, the code is horrendous, but functional. Use it, fork it, improve it, or laugh at it. No guarantees, no warranties, no promises. It works well enough to track buoys in San Francisco Bay, and that was the goal.

Features

Desktop

Desktop

Mobile

Mobile

  • Real-time Node Tracking: Live MQTT feed of mesh node positions
  • Interactive Map: Leaflet-based map with color-coded status markers
  • Responsive Design (v2.0 instrument-panel UI): works on desktop and mobile
    • Desktop: one-row bridge header + fleet rail (left) + full map
    • Mobile: bottom sheet over the map — collapsed one-line-per-buoy strip, drag up for full cards; the map stays visible
    • Automatic dark mode (follows the OS setting)
  • Node Details: Battery levels, hardware info, last-seen times, and channel information
  • Status Color-Coding: Blue (recent), Orange (stale), Red (very old)
  • Polling Progress Bar: Visual indicator in the header showing time until next data refresh
    • Green progress bar fills left to right, resets at each poll
    • Configurable polling interval (5-120 seconds; default is 10 seconds)
  • Time Indicators: Each buoy card shows labeled chips:
    • Fix: Time since the last GPS position packet
    • Heard: Time since any packet was received
    • Position History: Deduplicated by packet timestamp to show only unique positions (retransmitted packets are automatically filtered)
    • Position Trail Display: Shows movement history on map with markers fading from light blue (oldest) to dark blue (newest); size increases with recency
    • Server-side Deduplication: Position data reduced to one point per time window (configurable via data_limit_time in site.config)
      • Default: 1.0 hour (one point per hour)
      • Reduces 700+ daily points to ~24, saving 84% bandwidth
      • Adjust to 0.5 for 30-minute granularity or 24 for daily snapshots
  • Headline buoy states: each card leads with one word — On station / Moved / Stale / Muted 🔕 / No GPS yet — with labeled Fix / Heard / Batt chips; buoys needing attention sort to the top
  • Position Precision Validation: Automatically rejects GPS packets with degraded precision (precision_bits < 32), preventing corrupted relay packets from polluting position trails
  • Battery History: inline sparkline on every buoy card, plus a tap-to-open voltage chart with hover tooltips
  • Gateway Tracking: Automatically discovers and tracks mesh gateways
    • Shows all gateways that relay packets from your monitored nodes
    • Displays signal strength and status for each gateway connection
    • Identifies relay patterns and network coverage
  • Dynamic Controls: Real-time adjustment of tracker settings
    • Movement Threshold: Change alert distance (10-500m) without restarting
    • Trail History: Toggle position trail display and history length
    • Low Battery Threshold: Customize battery alert level
    • API Polling Interval: Adjust refresh rate
    • Email Kill Switch: Enable/disable alert emails instantly from the Control Menu — no restart needed
    • Per-Buoy Mutes: silence movement emails for a buoy that is deliberately away (yard, transport); auto-unmutes when it returns home. Map indication and battery alerts stay active
    • Settings changed in the UI persist across restarts (stored in the SQLite database); a Reset button returns to the config-file values
    • Server Restart: Clear all cached trail data and reset in-memory state from the Control Menu
  • Special Node Tracking: Track specific nodes with home positions and movement alerts
    • Green dashed rings show movement threshold (configurable)
    • Red solid rings when nodes move beyond threshold
    • Light red card background alerts when nodes move outside expected range
    • Gray markers at home position until first GPS fix
    • Packet activity display with timestamps

Quick Start

Installation

# Install dependencies
pip install -r requirements.txt

# v2.1 layered config: the app runs on built-in defaults out of the box.
# Create the two small override layers from the examples:
cp site.config.example config/site.config            # your fleet: buoys, homes, alert policy
cp environment.config.example config/environment.config  # your infra: broker, smtp, ports
cp secret.config.template config/secret.config       # credentials

nano config/site.config
nano config/environment.config

# Run the application
python3 run.py

The web interface will be available at http://localhost:5103

Docker Deployment (Recommended)

macOS note (Colima / Docker Desktop): the directory you run docker compose from must live on a path the Docker VM actually mounts (your home directory, by default). Running compose from a network/SMB volume makes the ./config and ./data bind mounts silently come up empty — the container then initializes from templates instead of your real config. Keep the deployment directory (compose file + volumes) under $HOME; building the image from a network-volume checkout is fine.

Option 1: Using Pre-built Docker Hub Image (Fastest)

No build or GitHub cloning required—pull the container, it initializes itself:

  1. Create volume directories:
mkdir -p config data logs
  1. Download docker-compose configuration from GitHub:
curl -o docker-compose.yml https://raw.githubusercontent.com/guthip/buoy-tracker/main/docker-compose.yml
  1. Start the service (first run will auto-initialize config files):
docker compose up -d
  1. Edit configuration files on the host:
nano config/site.config         # your fleet: buoys, homes, alert policy
nano config/environment.config  # your infra: broker, smtp, ports
nano config/secret.config       # credentials (if needed)
  1. Restart the container to apply changes:
docker compose restart

Access the web interface at http://localhost:5103

How it works:

  • Downloads pre-built image from Docker Hub (dokwerker8891/buoy-tracker:latest)
  • On first run, container auto-initializes config files from templates (included in image)
  • User edits config files directly on host (./config/ directory)
  • All data persists in mounted volumes (./config/, ./data/, ./logs/)
  • No GitHub clone needed—only docker-compose.yml and volumes directories

Option 2: Using docker-compose with Source Code (Build Locally)

Clone the repository and build from source:

  1. Clone the repository:
git clone https://github.com/guthip/buoy-tracker.git
cd buoy-tracker
  1. Create volume directories and configuration files from templates:
# Create directories for volumes (config, data, logs)
mkdir -p config data logs

# Create the two config layers from the examples
cp site.config.example config/site.config
cp environment.config.example config/environment.config
cp secret.config.template config/secret.config

# Customize
nano config/site.config         # your fleet: buoys, homes, alert policy
nano config/environment.config  # your infra: broker, smtp, ports
  1. Start the service:
docker compose up -d
  1. View logs:
docker compose logs -f

Access the web interface at http://localhost:5103

Volume Structure (persists between container restarts):

  • ./config/ → Configuration layers (site.config, environment.config, secret.config)
    • Mount to: /app/config in container
    • Editable on host; container reads from here
    • Create from templates during initial setup
  • ./data/ → Application data (buoy_tracker.db SQLite store: positions, telemetry, alert events, settings)
    • Mount to: /app/data in container
    • Persists between restarts
  • ./logs/ → Application logs
    • Mount to: /app/logs in container
    • Useful for debugging and monitoring

Making Changes to Configuration: After editing files in ./config/, restart the container to pick up changes:

docker compose restart buoy-tracker

Alternatively, use the Server Restart button in the Control Menu (🔐 requires API key) — this restarts the process via the web UI and clears all in-memory state.

Building Custom Images

If you want to build a custom image with local modifications:

# Clone the repository
git clone https://github.com/guthip/buoy-tracker.git
cd buoy-tracker

# Build the Docker image
docker build -t my-buoy-tracker:latest .

# Run with docker-compose (update service image in docker-compose.yml to my-buoy-tracker:latest)
docker compose up -d

What's Included:

  • ✅ Real-time Meshtastic mesh network node tracking
  • ✅ In-memory history (position and telemetry) for current session
  • ✅ Multi-platform: Works on Intel/AMD (x86_64), Apple Silicon (ARM64), Raspberry Pi (ARM64)

Configuration files (created from templates in config volume):

  • site.config.example / environment.config.example → commented reference layers (auto-placed in ./config/ on first run)
  • ./secret.config.template → Template showing required secrets; copy to config/secret.config and fill in real values

Generated directories:

  • ./config/ → Configuration files (created from templates during setup; mounted as volume for easy editing)
  • ./data/ → Application data persistence (buoy_tracker.db, the SQLite store for positions, telemetry, alerts, and settings)
  • ./logs/ → Application logs (created automatically)

Using the Interface

  • Node Sidebar: Click any node to zoom map to its location
  • Map Markers: Click markers for detailed popups with node information
  • Menu Controls (password-protected):
    • Toggle "Show Gateways & Connections" to show/hide gateway nodes and signal connections
    • Toggle "Show Position Trails" to visualize movement history on the map
    • Toggle "Show Nautical Markers" to display navigation markers on the map
    • Adjust trail history hours and movement threshold dynamically
    • Note: "Show All Nodes" is now a server-side config setting (show_all_nodes in environment.config), not a UI toggle
    • (Sorting is automatic and attention-first: buoys off location rank highest, then battery alarms, quiet nodes, and healthy buoys; gateways last)
  • Movement Alerts:
    • Green dashed circles show 50m threshold around special node home positions
    • Red solid circles appear when nodes exceed threshold
    • Card backgrounds turn light red when nodes move outside expected range
    • Browser alert on first threshold breach
  • Color Coding:
    • 🔵 Blue: Recent (< 1 hour, configurable via status_blue_threshold)
    • 🟠 Orange: Stale (1-12 hours, configurable via status_orange_threshold)
    • 🔴 Red: Very old (> 12 hours)
    • 🟡 Gold: Special node active
    • ⚫ Dark Gray: Special node stale
    • ⚪ Light Gray: Awaiting GPS (at home position)
    • 🔴 Light Red Card: Special node outside expected range
    • Fix/Heard chips use separate configurable thresholds (lpu_*/sol_* keys in [special_nodes_settings] in site.config):
      • Fix: blue < 3h, orange 3–8h, red > 8h (defaults for ~2-hour position update interval)
      • Heard: blue < 2h, orange 2–6h, red > 6h (defaults for ~1-hour telemetry interval)

Configuration

Before running the application, create your volume directories and configuration files:

# Create directories for volumes
mkdir -p config data logs

# Create the config layers from the examples
cp site.config.example config/site.config
cp environment.config.example config/environment.config
cp secret.config.template config/secret.config  # only if using auth/alerts

Applying Configuration Changes

After editing files in config/, restart the container:

docker compose restart

Configuration is layered (v2.1): built-in defaults < site.config (what the fleet is) < environment.config (where it runs) < secret.config < settings changed in the UI (persisted in the database). /health lists the loaded files under config_sources.

Upgrading from a single-file setup: run once python3 tools/split_config.py config/tracker.config — it writes the two layer files; the legacy file is ignored afterwards.

Key settings by layer:

MQTT Connection

[mqtt]
broker = mqtt.bayme.sh
port = 1883
root_topic = msh/US/bayarea/2/e/
channel_name = MediumFast
username = meshdev
password = large4cats

MQTT Subscription Optimization:

  • The channel_name parameter filters MQTT traffic to only the specified channel
  • Subscribes to: root_topic/channel_name/# (e.g., msh/US/bayarea/2/e/MediumFast/#)
  • Additional optimization: When both show_all_nodes=false AND show_gateways=false, the system subscribes only to specific special node topics
    • Example: msh/US/bayarea/2/e/MediumFast/!db8e8f6d/# (one subscription per special node)
    • Dramatically reduces bandwidth by filtering at the MQTT broker level
    • For 4 special nodes: only receives those 4 nodes' packets, ignoring hundreds of other mesh nodes
  • Toggling show_gateways in the UI automatically reloads MQTT subscriptions (no restart needed)

Web Interface

[webapp]
host = 127.0.0.1
port = 5103

# Subpath deployments need no config here (there is no url_prefix key).
# The app detects the prefix per request from the X-Forwarded-Prefix header,
# which Traefik's stripprefix middleware sends automatically; for
# nginx/Apache add one proxy header line (see DOCKER.md). One container can
# serve a subdomain and a subpath simultaneously.

# Map center point. Supports both decimal and degrees-minutes formats:
# Decimal: default_center = 37.7749,-122.4194
# Degrees-minutes: default_center = N37° 33.81', W122° 13.13'
default_center = 37.7749,-122.4194
default_zoom = 13

# Node status color thresholds (in hours)
# Less than status_blue_threshold = blue (recent)
# Between blue and orange = orange (stale)
# Older than status_orange_threshold = red (very stale)
status_blue_threshold = 1
status_orange_threshold = 12

# Data polling interval (in seconds)
# How often the client polls the server for updates (applies to all endpoints)
# Default: 10 seconds (fast updates; rate limit auto-scales based on polling interval and special nodes)
# Range: 5-120 seconds (validated on startup)
# Note: Actual requests per interval = 3 base endpoints + N special nodes with trails enabled
# Examples (with 4 special nodes):
#   5 seconds  = 7,200/hour (aggressive, high server load)
#   10 seconds = 3,600/hour (default, frequent updates, excellent for demos)
#   30 seconds = 1,200/hour (balanced)
#   60 seconds = 600/hour (conservative)
#   120 seconds = 300/hour (low load)
# ⚠️ Progress bar in UI updates every 100ms, filling from 0-100% over the polling interval
api_polling_interval = 10

User Interface Controls (Admin-Controlled)

Lock down the user interface to prevent end users from modifying settings. This is useful for public deployments where you want consistent configuration across all users:

[app_features]
# Server-side performance settings (require restart to change)
show_all_nodes = false  # false=only special+gateways (faster), true=all mesh nodes (slower)

# UI feature flags (users can modify these via Controls tab)
show_gateways = true           # Toggleable in UI, reloads MQTT subscriptions dynamically
show_position_trails = true    # Toggleable in UI
show_nautical_markers = true   # Toggleable in UI
trail_history_hours = 168      # Adjustable in UI

Access Model:

  • ✅ All users can view the map and data
  • 🔐 Control Menu (settings modifications) requires password authentication
  • API key is configured in secret.config under [webapp] api_key

Dynamic Settings:

  • show_gateways: When toggled in UI, automatically reloads MQTT subscriptions
    • Turning OFF reduces bandwidth by subscribing only to special node topics
    • Turning ON subscribes to all nodes on the channel
    • No server restart required

API rate limits are automatically calculated based on polling interval and number of special nodes:

  • Formula: (3600 / polling_seconds) * (3_base_endpoints + N_special_nodes) * 2.0_safety_margin, rounded up to nearest 10
  • Base endpoints: /health, api/nodes, api/special/history/batch = 3 requests per interval
  • Per special node: api/special/history request when trails enabled = N additional requests
  • Safety multiplier: 2.0x provides headroom for traffic spikes
  • Examples (assuming 4 special nodes configured):
    • At 10-second polling → 3,600 requests/hour per IP address (current default)
    • At 30-second polling → 1,200 requests/hour per IP address
    • At 60-second polling → 600 requests/hour per IP address
  • Dynamic scaling: Rate limit automatically adjusts if you add/remove special nodes in site.config
  • Client Notification: If a client exceeds the rate limit:
    • The progress bar turns orange and displays remaining pause time
    • Polling automatically pauses for 60 seconds, then resumes
    • Browser console shows [RATELIMIT] messages for debugging

Progress Bar Indicator:

  • Located in the header bar
  • Green fill shows time elapsed since last data poll
  • Fills 0→100% over the polling interval
  • Orange display during rate limit pause with countdown
  • Updates every 100ms for smooth animation

The rate limit automatically adjusts based on polling interval and special node count:

# Change polling frequency in environment.config:
api_polling_interval = 10   # Calculates rate limit from polling interval and special nodes in config
api_polling_interval = 30   # Lower polling = lower rate limit
api_polling_interval = 60   # Even more conservative
api_polling_interval = 120  # Very conservative, minimal server load

Why auto-calculation?

  • Rate limit stays proportional to polling frequency
  • Scales with number of special nodes configured
  • No need to manually adjust multiple settings
  • Prevents users from setting aggressive polling with restrictive rate limits
  • One config value controls both behavior

Reverse Proxy & Subpath Deployment

Deploy Buoy Tracker behind a reverse proxy (nginx, Apache, etc.) at any subpath. This is useful for:

  • Shared servers: Run multiple apps on one domain
  • Subdirectory hosting: Host at example.com/buoy-tracker/ instead of subdomain
  • HTTPS termination: Let reverse proxy handle SSL/TLS
  • Load balancing: Distribute traffic across multiple instances

Configuration

No app configuration needed (v2.1): the app reads the X-Forwarded-Prefix header per request. Traefik's stripprefix middleware sends it automatically. For Apache/nginx add one line:

# Apache (inside the proxied <Location>)
RequestHeader set X-Forwarded-Prefix "/buoy-tracker"
# nginx (inside the location block)
proxy_set_header X-Forwarded-Prefix /buoy-tracker;

Nginx Example

server {
    listen 443 ssl http2;
    server_name example.com;
    
    ssl_certificate /path/to/cert.pem;
    ssl_certificate_key /path/to/key.pem;
    
    # Buoy Tracker at /buoy-tracker/
    location /buoy-tracker/ {
        proxy_pass http://localhost:5103/;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        
        # WebSocket support (for future real-time features)
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }
}

Key Points:

  • Send X-Forwarded-Prefix: /buoy-tracker from the proxy (one line, above)
  • Nginx location path must end with /
  • proxy_pass to Flask app port (e.g., http://localhost:5103/)
  • Don't add the prefix to the proxy_pass URL—Flask handles that via blueprint registration
  • App is accessible at https://example.com/buoy-tracker/

Apache Example

<VirtualHost *:443>
    ServerName example.com
    SSLEngine on
    SSLCertificateFile /path/to/cert.pem
    SSLCertificateKeyFile /path/to/key.pem
    
    # Buoy Tracker at /buoy-tracker/
    <Location /buoy-tracker/>
        ProxyPass http://localhost:5103/
        ProxyPassReverse http://localhost:5103/
        ProxyPreserveHost On
        
        # WebSocket support
        RewriteEngine On
        RewriteCond %{HTTP:Upgrade} websocket [NC]
        RewriteCond %{HTTP:Connection} upgrade [NC]
        RewriteRule ^/buoy-tracker/(.*) "ws://localhost:5103/$1" [P,L]
    </Location>
</VirtualHost>

Troubleshooting:

Problem Cause Solution
404 on all API calls Proxy isn't sending X-Forwarded-Prefix Add the header line shown above for your proxy
Routes work but styles/JS broken Prefix only applied to Flask routes, not static files Ensure reverse proxy serves /static/ from root Flask app
CORS errors Missing headers Verify reverse proxy forwards X-Forwarded-* headers
Connection lost immediately Flask can't detect reverse proxy Ensure X-Forwarded-For header is passed through

Special Nodes

Track specific nodes with extra detail:

[special_nodes_settings]
stale_after_hours = 12
special_symbol = ⭐

# Format: node_id = label,home_lat,home_lon,has_power_sensor,voltage_channel
#   - label: Display name for the node
#   - home_lat, home_lon: Expected location (optional - if omitted, first position becomes origin)
#   - has_power_sensor: 'true' for nodes with INA260/INA219 power sensor (shows voltage instead of battery %)
#   - voltage_channel: Which voltage to use (optional - defaults: 'ch3_voltage' for power sensors, 'device_voltage' for others)
#       * 'ch3_voltage' = Battery voltage (INA260 channel 3)
#       * 'ch1_voltage' = Input voltage (INA260 channel 1, e.g., solar/USB)
#       * 'device_voltage' = Device reported voltage
# Coordinates support two formats:
#   - Decimal degrees: 37.5637125,-122.2189855
#   - Degrees-minutes: N37° 33.81', W122° 13.13'

# Examples with decimal format
3681533965 = SYCS,37.5637125,-122.2189855,true
492590216 = SYCE,37.5806826,-122.2175423

# Examples with degrees-minutes format
3681533965 = SYCS,N37° 33.81',W122° 13.13',true,ch3_voltage
2512106321 = SYCA,N37° 31.94',W122° 10.31'

Coordinate Formats:

  • Decimal Degrees: Standard latitude/longitude format (e.g., 37.5637125,-122.2189855)
  • Degrees-Minutes: Navigation format with hemisphere prefix (e.g., N37° 33.81', W122° 13.13')
    • North/South for latitude (N = positive, S = negative)
    • East/West for longitude (E = positive, W = negative)
    • Format: [NSEW]degrees° minutes'
    • Important: Separate latitude and longitude with a comma

Auto-Learn Origin (Optional): If you omit home coordinates in the config, the system automatically learns the origin from the first GPS position the node reports. This is useful when you don't have a precise home location yet:

[special_nodes]
# With coordinates (fixed origin for movement tracking)
3681533965 = SYCS,N37° 33.81',W122° 13.13',true

# Without coordinates (learns from first GPS position received)
492590216 = SYCE

# Label only (no location, no power sensor)
2512106321 = SYCA

Once a position is learned, movement alerts are triggered relative to that first position. The origin updates if you later add home coordinates to the config.

Movement Alerts: Green dashed ring shows threshold boundary. Red solid ring appears when node moves beyond threshold from home position.

Fix and Heard Timing Thresholds: The Fix (last GPS position) and Heard (any packet) chips use independent thresholds configured in [special_nodes_settings] (key names keep the historical lpu/sol prefixes):

[special_nodes_settings]
# Fix thresholds: color based on time since last GPS position packet
lpu_blue_threshold_hours = 3       # Green/blue within 3 hours
lpu_orange_threshold_hours = 8     # Orange between 3-8 hours
# Red if older than 8 hours

# Heard thresholds: color based on time since any packet (telemetry, position, etc.)
sol_blue_threshold_hours = 2       # Green/blue within 2 hours
sol_orange_threshold_hours = 6     # Orange between 2-6 hours
# Red if older than 6 hours

Tune these to your buoy's update interval — the defaults assume ~2-hour position updates and ~1-hour telemetry.

Email Alerts: Configure email notifications when nodes move outside the fence (see Email Alerts section below).

Data Handling (v2.0): every accepted position, telemetry reading, and alert decision is recorded in a SQLite database at data/buoy_tracker.db (90-day retention for measurements, configurable via [database] retention_days). Position trails rebuild from it automatically at startup. Open the file read-only with any SQLite tool (DBeaver, pandas, DuckDB, Datasette, Grafana). For a live Docker deployment, query inside the container — e.g. tools/dbq.sh "SELECT COUNT(*) FROM positions" (works on any image) or docker exec buoy-tracker sqlite3 /app/data/buoy_tracker.db "..." (sqlite3 CLI included in the image from v2.0) — or copy the file first; querying the mounted file from the host while the container writes is best avoided for analysis — never write to it while the app is running.

  • When false (default): Historical data is NOT loaded from disk on startup - start fresh (recommended for production)
  • When true: Load any existing historical data from disk on startup (development/debugging)
  • Either way, new data collected is saved to disk for future reference
  • Packet data includes: timestamps, packet types, channel info, position/telemetry/nodeinfo details

Email Alerts

Send email notifications when special nodes move outside their home fence.

⚠️ Platform-Specific Setup Required - Email delivery method depends on your deployment environment.

How It Works

  • Continuous Monitoring: Alerts are sent whenever a special node is outside its safe zone
  • Smart Cooldown: Only one email per node per cooldown period (default 1 hour, configurable)
  • No Redundant Alerts: If a node stays outside the zone, you get one alert per cooldown period, not continuous emails
  • Includes: Distance from home, battery level, timestamp, and tracker URL

Platform-Specific Configuration

Production Deployment (Linux Servers) - RECOMMENDED

Linux servers have sendmail or postfix running by default. Use localhost:25 (no credentials needed):

In config/environment.config (site policy like enabled goes in site.config):

[alerts]
enabled = true
alert_cooldown = 1

tracker_url = http://your-server-address:5103
email_from = noreply@example.com

# SMTP Configuration for localhost:25 (sendmail/postfix)
smtp_host = localhost
smtp_port = 25
smtp_ssl = false
# No credentials needed for sendmail/postfix

In secret.config:

[alerts]
# Email recipient address(es)
email_to = your-email@example.com

Verify sendmail is running:

sudo systemctl status sendmail
# or
sudo systemctl status postfix

# If not installed:
sudo apt install sendmail  # Debian/Ubuntu
# or
sudo yum install sendmail   # RHEL/CentOS

Development Setup (Mac/Windows) - External SMTP Required

macOS and Windows don't have sendmail/postfix running by default. Use an external SMTP provider:

In config/environment.config (site policy like enabled goes in site.config):

[alerts]
enabled = true
alert_cooldown = 1

tracker_url = http://localhost:5103
email_from = noreply@example.com

# Override SMTP settings for external provider
smtp_host = smtp.gmail.com
smtp_port = 587
smtp_ssl = false

In secret.config:

[alerts]
# Email recipient address(es)
email_to = your-email@example.com

# SMTP credentials (required for external providers)
smtp_username = your-email@gmail.com
smtp_password = your-app-password

See External SMTP Providers section below for setup instructions.

External SMTP Providers

For development on Mac/Windows, use an external SMTP provider. All providers work the same way - configure in config/environment.config:

Gmail:

[alerts]
smtp_host = smtp.gmail.com
smtp_port = 587
smtp_ssl = false

SendGrid:

[alerts]
smtp_host = smtp.sendgrid.net
smtp_port = 587
smtp_ssl = false

AWS SES:

[alerts]
smtp_host = email-smtp.us-west-2.amazonaws.com
smtp_port = 587
smtp_ssl = false

Then add credentials to secret.config:

[alerts]
smtp_username = your-email@gmail.com
smtp_password = your-app-password
email_to = recipient@example.com

Security: Environment Variables

For production, use environment variables instead of storing credentials in config:

export ALERT_SMTP_USERNAME="your-email@gmail.com"
export ALERT_SMTP_PASSWORD="your-app-password"

Then leave smtp_username and smtp_password blank in the config files.

Testing

Test your email configuration using the /api/test-alert endpoint.

For Local/Development Environment:

If you have NO api_key set in secret.config (development mode):

curl -X POST http://localhost:5103/api/test-alert \
  -H "Content-Type: application/json" \
  -d '{"type": "movement"}'

If you DO have an api_key in secret.config (most common):

curl -X POST http://localhost:5103/api/test-alert \
  -H "Authorization: Bearer YOUR_API_KEY_HERE" \
  -H "Content-Type: application/json" \
  -d '{"type": "movement"}'

Valid alert types: "movement", "battery", "offline"

For Production/Remote Environment:

curl -X POST https://your-domain.com/api/test-alert \
  -H "Authorization: Bearer YourPasswordHere" \
  -H "Content-Type: application/json" \
  -d '{"type": "movement"}'

API Reference

Authentication

Access Model:

  • Read-only endpoints (map view, data access) are always public - no authentication required
  • Protected endpoints (Control Menu actions) require API key authentication via Authorization: Bearer <api_key> header
  • /health endpoint is always public (used by Docker healthcheck)

Configuration:

  • API key is stored in secret.config under [webapp] api_key
  • In development mode (ENV=development): localhost requests are automatically exempted from auth
  • In production mode: all protected endpoint requests require valid API key

Core Endpoints

  • GET /health - Health check with MQTT status and config (always public, no auth)
  • GET /api/nodes - All tracked nodes with position, battery, channel

Special Node Endpoints

  • GET /api/special/history/batch?hours=<hours> - Position history for all special nodes (single batch request)
  • GET /api/signal/history?node_id=<id> - Battery history for a node

Mute & Settings Endpoints (v2.0)

  • GET /api/alerts/mutes - Per-buoy movement-alert mute states (public read)
  • POST /api/alerts/mute - Mute/unmute one buoy's movement emails (Bearer)
    • Body: {"node_id": 123, "muted": true}
  • POST /api/settings/reset - Delete runtime-setting overrides, restore config-file defaults (Bearer)

Debug & Simulation API (v2.0, off by default)

Set [debug] enable_simulation = true (never in production) to activate POST /api/debug/inject, POST /api/debug/scenario, POST /api/debug/replay, and GET /api/debug/state — synthetic packet streams through the real pipeline for fast alert debugging. Endpoints return 404 when disabled and always require the API key. Alert emails are dry-run (logged, not sent) in simulation mode.

Admin Endpoints (Protected - Require Authentication)

  • POST /api/config/movement-threshold - Update movement threshold for special nodes

    • Body: {"threshold": 100} (distance in meters)
    • Returns: {"success": true, "threshold": 100}
  • POST /api/test-alert - Send test alert email to verify email configuration

    • Body: {"type": "movement"}, {"type": "battery"}, or {"type": "offline"} (optional, defaults to "movement")
    • Returns: {"success": true, "message": "Test movement alert sent"}
    • Requires: Email alerts enabled in site.config ([alerts] enabled = true)
    • Authentication: Required if api_key is set in secret.config (bypassed on localhost if ENV=development)
    • Use cases: Verify SMTP configuration, test email delivery, check alert formatting

Examples:

# Production/Remote - Always requires Authorization header
curl -X POST https://your-domain.com/api/test-alert \
  -H "Authorization: Bearer YourPasswordHere" \
  -H "Content-Type: application/json" \
  -d '{"type": "movement"}'

# Localhost with api_key configured - Still requires Authorization
curl -X POST http://localhost:5103/api/test-alert \
  -H "Authorization: Bearer YOUR_API_KEY_HERE" \
  -H "Content-Type: application/json" \
  -d '{"type": "battery"}'

# Localhost without api_key in secret.config - No auth needed
curl -X POST http://localhost:5103/api/test-alert \
  -H "Content-Type: application/json" \
  -d '{"type": "offline"}'

Project Structure

buoy_tracker/
├── src/
│   ├── main.py              # Flask app and routes
│   ├── mqtt_handler.py      # MQTT client and message handlers
│   ├── config.py            # Configuration loader
│   └── __init__.py
├── templates/
│   └── simple.html          # Web UI (Leaflet map)
├── static/
│   └── app.js               # Frontend JavaScript
├── data/                    # Application data (persisted; special node history)
├── tests/                   # Test suite
├── site.config.example      # Fleet-layer reference (buoys, homes, policy)
├── environment.config.example # Infrastructure-layer reference
├── secret.config.template   # Template (copy to secret.config during setup)
├── run.py                   # Application runner
└── requirements.txt         # Python dependencies

Note: the real site.config, environment.config, and secret.config live in each deployment's config/ volume and are never committed.

Development

Running Tests

pytest tests/

Technology Stack

  • Python 3.13+ with Flask 3.x
  • Meshtastic MQTT JSON library
  • Leaflet.js + OpenStreetMap
  • paho-mqtt for MQTT client

Code Style

This project follows PEP 8 guidelines. Use black for code formatting and flake8 for linting.

Contributing

  1. Create a feature branch
  2. Make your changes
  3. Write/update tests
  4. Submit a pull request

License & Attribution

Buoy Tracker is licensed under the GNU General Public License v3.0 (GPL v3).

This project builds upon several excellent open source libraries:

  • Meshtastic (GPL v3) - Mesh networking protocol
  • Flask (BSD 3-Clause) - Web framework
  • Leaflet.js (BSD 2-Clause) - Interactive maps
  • OpenStreetMap (ODbL 1.0) - Map tiles and data

For complete attribution and license details, see:

GPL v3 Requirements

If you modify or redistribute Buoy Tracker, you must:

  • Maintain GPL v3 or compatible license
  • Provide source code
  • Document all modifications
  • Retain all license notices

We recommend GPL v3 for derived works to ensure improvements benefit the community.

Support

For issues and questions, please open an issue on the repository.

About

A real-time web interface for tracking Meshtastic mesh network nodes on a live map.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages