Authors: Owen Ruzanski (KD3ALD), Liam Miller (KD3BVX), Nathaniel Frissell (W2NAF) Organization: University of Scranton (W3USR), Frankford Radio Club Project: HamSCI Personal Space Weather Station Dashboard Development Last Updated: January 2026
- Project Overview
- System Architecture
- Installation
- Configuration
- API Documentation
- Frontend Components
- Database Schema
- Development Guide
- Deployment
- Troubleshooting
The HamSCI Contesting and DXing Dashboard is a real-time web application designed to help amateur radio operators optimize their HF transmissions during contests and DX operations. The dashboard visualizes propagation data from the HamSCI Personal Space Weather Station (PSWS) network, which collects WSPR, FT8, and FT4 digital mode spots.
- Real-time propagation visualization on an interactive world map
- Band-specific filtering for all amateur radio HF bands (160m-10m)
- Geographic filtering by country, continent, CQ zone, and ITU zone
- Mode filtering supporting WSPR, FT8, and FT4
- Tabular view showing band openings by geographic region
- Auto-reload capability with configurable intervals
- Contest bands mode focusing on the 6 primary contest bands
- Session persistence for filter settings across page reloads
- Enable amateur radio operators to optimize transmissions based on real-time ionospheric conditions
- Provide local propagation insights to individual PSWS stations
- Contribute to understanding of both localized and global HF conditions
- Answer key questions operators ask:
- What bands are open and where?
- When did they open/close?
- What is the current Maximum Usable Frequency (MUF)?
- Which band has the most activity?
- What direction are signals coming from?
Backend:
- Python 3.x
- Flask 2.x (web framework)
- MongoDB (database)
- PyMongo (database driver)
- Additional libraries: maidenhead, geopandas, shapely, geopy
Frontend:
- HTML5/CSS3
- JavaScript (ES6+)
- Leaflet.js (interactive maps)
- Leaflet.ExtraMarkers (custom marker icons)
- Turf.js (geospatial analysis)
Data Sources:
- MongoDB database running WSPRDaemon
- GeoJSON boundary files (countries, continents, CQ zones, ITU zones)
┌─────────────────────────────────────────────┐
│ PSWS Receiver (KD3ALD) │
│ RX-888 SDR + KA9Q-radio + WSPRDaemon │
│ Decodes WSPR/FT8/FT4 on all HF bands │
└─────────────────┬───────────────────────────┘
│
↓
┌─────────────────────────────────────────────┐
│ MongoDB Database Server │
│ wspr_db.spots │
│ Stores: callsign, grid, frequency, SNR, │
│ date, time, mode, drift │
└─────────────────┬───────────────────────────┘
│
↓
┌─────────────────────────────────────────────┐
│ Flask Backend (web-ft.py) │
│ Routes: │
│ / → both.html (combined view) │
│ /map → index_ft.html (map) │
│ /table → table_ft.html (table) │
│ /spots → JSON API (map data) │
│ /tbspots → JSON API (table data) │
└─────────────────┬───────────────────────────┘
│
↓
┌─────────────────────────────────────────────┐
│ Frontend JavaScript │
│ ┌──────────────┐ ┌───────────────┐ │
│ │ map_ft.js │ │ table_ft.js │ │
│ │ (730 lines) │ │ (183 lines) │ │
│ │ - Leaflet │ │ - Regional │ │
│ │ - Filtering │ │ aggregation│ │
│ │ - Markers │ │ - Band matrix│ │
│ └──────────────┘ └───────────────┘ │
└─────────────────────────────────────────────┘
│
↓
┌─────────────────────────────────────────────┐
│ User's Web Browser │
│ Interactive map or table view with │
│ real-time spot updates every 2-30 min │
└─────────────────────────────────────────────┘
- Data Collection: PSWS receiver decodes WSPR/FT8/FT4 spots continuously
- Storage: Spots written to MongoDB with metadata (callsign, grid, frequency, SNR, time, mode)
- API Request: Frontend requests spots via
/spots?lastInterval=15 - Processing: Backend queries MongoDB for spots in time window, converts grids to coordinates
- Client Filtering: JavaScript applies band/country/zone/mode filters
- Visualization: Spots rendered on map with colored markers or in table by region
✅ Security Update (January 2026): This repository's git history has been cleaned to remove all previously exposed credentials. All passwords and IP addresses have been redacted from historical commits.
Important security practices:
- Environment variables required: This application uses
.envfiles for credential management (never commit.envto git) - Change default passwords: If you're deploying this, always use unique, strong passwords
- Review
.env.example: Copy it to.envand configure with your credentials - Git history is clean: All sensitive data has been replaced with
***REDACTED***in past commits
- Python 3.8 or higher
- MongoDB 4.x or higher (with WSPRDaemon database)
- Modern web browser (Chrome, Firefox, Safari, Edge)
- PSWS receiver system (RX-888 + KA9Q-radio + WSPRDaemon)
Create a virtual environment and install dependencies:
# Create virtual environment
python -m venv venv
# Activate virtual environment
# On Windows:
venv\Scripts\activate
# On Linux/Mac:
source venv/bin/activate
# Install dependencies from requirements.txt
pip install -r requirements.txtSee requirements.txt for the complete list:
- flask>=2.0.0 - Web framework
- pymongo>=4.0.0 - MongoDB driver
- maidenhead>=1.1.0 - Grid square conversion
- geopandas>=0.10.0 - Geospatial data
- shapely>=1.8.0 - Geometric operations
- geopy>=2.2.0 - Geocoding
- python-dotenv>=0.19.0 - Environment variable management
The following GeoJSON files must be present in static/js/:
countries.geojson(14MB) - Country boundariescontinents.geojson(4KB) - Continent boundariescqzones.geojson(2.7MB) - CQ zone polygons (40 zones)ituzones.geojson(1.5MB) - ITU zone polygons (90 zones)
These files are included in the repository.
The application uses environment variables for secure credential management.
-
Copy the example environment file:
cp .env.example .env
-
Edit
.envand set your MongoDB credentials:MONGODB_HOST=your_mongodb_host MONGODB_PORT=27017 MONGODB_USERNAME=your_username MONGODB_PASSWORD=your_password MONGODB_DATABASE=wspr_db
IMPORTANT: Never commit the .env file to git! It's already in .gitignore.
Update the receiver callsign and grid square in:
Backend (web-ft.py):
rxlat, rxlon = maidenhead.to_location("FN21ni") # Change to your gridFrontend Table View (static/js/table_ft.js):
const call = "KD3ALD" // Change to your callsignFrontend Map View (static/js/map_ft.js):
title.textContent = `WSPR Spots From ${spot.rx_sign} PSWS Receiver`Band colors are defined in static/js/map_ft.js:
const bandColorMap = {
'160m': 'black',
'80m': 'red',
'40m': 'orange',
'20m': 'green',
'15m': 'cyan',
'10m': 'blue-dark',
// ... etc
};Fetch spots for map display with full propagation details.
Query Parameters:
lastInterval(string, default: "15") - Minutes to look back from current time
Response Format:
[
{
"tx_sign": "W1ABC",
"tx_lat": 42.3601,
"tx_lon": -71.0589,
"rx_sign": "KD3ALD",
"rx_lat": 40.7589,
"rx_lon": -74.2215,
"frequency": 14.097,
"band": "20m",
"mode": "wspr",
"snr": -12,
"drift": 0,
"time": "260107 1430"
}
]Example Request:
curl "http://localhost:5000/spots?lastInterval=30"Fetch spots for table display with regional aggregation data.
Query Parameters:
lastInterval(string, default: "15") - Minutes to look back
Response Format:
[
{
"id": "$507f1f77bcf86cd799439011",
"band": "20m",
"grid": "FN42hx",
"time": "260107 1430",
"cq_zone": 5,
"mode": "wspr"
}
]Files: templates/index_ft.html, static/js/map_ft.js
Features:
- Interactive Leaflet map centered at lat 20°, lon 0°
- Band-specific colored star markers
- Polylines connecting TX and RX stations
- Clickable markers with spot details
- Real-time spot counter by band (bottom-right)
- CQ zone outline overlay with zone labels
Filtering Controls:
- Time interval (last N minutes)
- Band selection (all bands or contest bands only)
- Country filter (including "Non-US" option)
- Continent filter
- CQ zone filter (1-40)
- ITU zone filter (1-90)
- Mode checkboxes (WSPR/FT8/FT4)
- CQ zone outline toggle
- Auto-reload interval (2-30 minutes)
Key Functions:
loadSpots()- Main function that fetches and renders spotslookupCountry(lat, lon)- Point-in-polygon country lookuplookupCqZone(lat, lon)- CQ zone identificationfrequencyToBand(freq)- Converts MHz to band nameparseWsprTime(str)- Parses "YYMMDD HHMM" to ISO format
Performance Notes:
- Loads large GeoJSON files asynchronously (~18MB total)
- Client-side filtering prevents excessive server load
- Session storage preserves filter state
Files: templates/table_ft.html, static/js/table_ft.js
Features:
- Matrix display: Regions (rows) × Bands (columns)
- Shows spot counts for 6 contest bands only
- Green highlighting for bands meeting threshold
- Dual-column layout (16 regions in 8 rows)
- Total spot counter
Region Mapping:
- Maps CQ zones to 14 geographic regions
- Examples: Europe (zones 14-16,20), North America (zones 1-7,40)
- Optimized for contest operations
Key Functions:
loadSpots()- Fetches data from/tbspotsendpointbuildTable(counts, bands, threshold)- Generates HTML tablegetRegionFromCQ(zone)- Maps CQ zone to region nameparseTableTime(t)- Parses timestamp strings
Configuration:
- Threshold: Minimum spots to highlight (default: 1)
- Interval: Last N minutes to display (default: 15)
Each document represents one decoded WSPR/FT8/FT4 spot.
Schema:
{
_id: ObjectId("..."),
callsign: "W1ABC", // Transmitter callsign
rx_callsign: "KD3ALD", // Receiver callsign
grid: "FN42hx", // 6-character Maidenhead grid square
frequency: 14.097062, // Frequency in MHz
band: "20m", // Band designation
mode: "wspr", // Mode: "wspr", "ft8", or "ft4"
snr: -15, // Signal-to-noise ratio in dB
drift: 0, // Frequency drift in Hz
date: "260107", // Date in YYMMDD format
time: "1430" // Time in HHMM UTC format
}Indexes:
- Compound index on
(date, time)for efficient time-based queries - Index on
modefor mode-specific queries
Data Source:
- Spots are inserted by WSPRDaemon software running on the PSWS receiver
- Typical rate: 10-100 spots per 2-minute WSPR/FT8 cycle
- Database retention: Configurable (typically 30-90 days)
frc_contesting/
├── _archive/ # Legacy/deprecated code
│ ├── legacy_python/ # Old Flask variants
│ ├── legacy_javascript/ # Old JS implementations
│ └── legacy_templates/ # Old HTML templates
├── static/ # Frontend assets
│ ├── css/
│ │ └── style.css # Custom styles
│ ├── img/ # Marker images
│ └── js/
│ ├── map_ft.js # Map visualization (730 lines)
│ ├── table_ft.js # Table view (183 lines)
│ ├── chart.js # Spot counting (98 lines)
│ ├── countries.geojson # Country boundaries (14MB)
│ ├── continents.geojson # Continent boundaries (4KB)
│ ├── cqzones.geojson # CQ zones (2.7MB)
│ ├── ituzones.geojson # ITU zones (1.5MB)
│ └── turf.min.js # Geospatial library (591KB)
├── templates/ # Flask HTML templates
│ ├── both.html # Combined map + table view
│ ├── index_ft.html # Map view
│ ├── table_ft.html # Table view
│ └── index_wcount.html # Alternative display with counter
├── web-ft.py # Main Flask app (WSPR+FT8+FT4)
├── CONTRIBUTING.md # Contribution guidelines
├── OPERATOR_GUIDE.md # User guide for operators
└── README.md # This file
# Activate virtual environment
source venv/bin/activate # or venv\Scripts\activate on Windows
# Run Flask development server
python web-ft.py
# Server will start at http://localhost:5000
# Debug mode is enabled by defaultAccess Points:
- http://localhost:5000/ - Combined view (map + table)
- http://localhost:5000/map - Map view only
- http://localhost:5000/table - Table view only
- http://localhost:5000/spots?lastInterval=15 - Raw JSON data
Backend Changes (Python):
- Edit web-ft.py
- Flask auto-reloads in debug mode
- Test API endpoints with curl or browser
Frontend Changes (JavaScript/HTML):
- Edit files in
static/ortemplates/ - Hard refresh browser (Ctrl+F5) to clear cache
- Use browser DevTools Console to debug JavaScript
Database Changes:
- Connect to MongoDB:
mongo --host $MONGODB_HOST --port $MONGODB_PORT -u $MONGODB_USERNAME -p - Query spots:
db.spots.find().sort({date: -1, time: -1}).limit(10)
To add a new client-side filter:
-
Add UI control in templates/index_ft.html:
<select id="myNewFilter"> <option value="">All</option> <option value="value1">Option 1</option> </select>
-
Read filter value in static/js/map_ft.js
loadSpots():const myFilter = document.getElementById("myNewFilter").value;
-
Apply filter logic in the spot rendering loop:
if (myFilter && spot.property !== myFilter) { return; // Skip this spot }
-
Persist in session storage (optional):
sessionStorage.setItem("myFilter", myFilter); const saved = sessionStorage.getItem("myFilter");
# Install gunicorn
pip install gunicorn
# Run with 4 worker processes
gunicorn -w 4 -b 0.0.0.0:5000 web-ft:app
# Or with systemd service
sudo systemctl start hamsci-dashboardCreate /etc/systemd/system/hamsci-dashboard.service:
[Unit]
Description=HamSCI Contesting Dashboard
After=network.target
[Service]
User=hamsci
Group=hamsci
WorkingDirectory=/opt/hamsci-dashboard
Environment="PATH=/opt/hamsci-dashboard/venv/bin"
ExecStart=/opt/hamsci-dashboard/venv/bin/gunicorn -w 4 -b 0.0.0.0:5000 web-ft:app
[Install]
WantedBy=multi-user.targetserver {
listen 80;
server_name dash.kd3ald.com;
location / {
proxy_pass http://127.0.0.1:5000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
location /static {
alias /opt/hamsci-dashboard/static;
expires 1h;
}
}- MongoDB Authentication: Use strong passwords and restrict network access
- HTTPS: Use Let's Encrypt certificates for production
- CORS: Configure if frontend is served from different domain
- Rate Limiting: Implement to prevent API abuse
- Input Validation: All user inputs are currently strings, consider validation
Error: ServerSelectionTimeoutError: <host>:<port>
Solutions:
- Check MongoDB server is running:
systemctl status mongod - Verify network connectivity:
ping $MONGODB_HOST - Check firewall rules allow the configured port
- Verify credentials in your
.envfile match the MongoDB server configuration - Ensure
MONGODB_PASSWORDenvironment variable is set
Symptoms: Map or table shows "Found 0 spots"
Debugging:
- Check raw API response:
curl http://localhost:5000/spots?lastInterval=60 - Verify spots exist in database:
db.spots.find().sort({date: -1, time: -1}).limit(5)
- Check time filtering - database stores UTC times
- Verify receiver is actively decoding (check WSPRDaemon logs)
Error (Console): Failed to load countries.geojson
Solutions:
- Verify files exist in
static/js/directory - Check file permissions (readable by web server)
- Look for CORS errors in browser console
- Verify Flask static folder is configured correctly
Symptoms: Polylines visible but no TX markers
Debugging:
- Check browser console for JavaScript errors
- Verify Leaflet.ExtraMarkers library loaded:
L.ExtraMarkers - Check marker image files exist in
static/img/ - Verify band color mapping includes all bands in data
Error: maidenhead.to_location() exception
Cause: Database contains malformed grid squares
Solutions:
- Backend catches exception and defaults to (0, 0)
- To identify bad grids:
import maidenhead for doc in collection.find(): try: maidenhead.to_location(doc['grid']) except: print(f"Invalid grid: {doc['grid']} from {doc['callsign']}")
Symptoms: Filters reset on page reload
Solutions:
- Check browser allows session storage (not in incognito mode)
- Verify JavaScript console for storage quota errors
- Clear browser cache and try again
- Check session storage in DevTools → Application → Session Storage
Slow Map Loading:
- GeoJSON files are large (~18MB total)
- Consider using CDN for GeoJSON files
- Implement caching headers in nginx
- Use compressed versions (.geojson.gz)
High Memory Usage:
- Leaflet keeps all markers in memory
- For >1000 spots, consider marker clustering:
var markers = L.markerClusterGroup();
Slow Database Queries:
- Add compound index on (date, time):
db.spots.createIndex({date: -1, time: -1})
- Consider materialized views for table data
- Requirements Document - Formal requirements specification
- Claude AI Assistance Documentation - AI contribution history and guidelines
- Operator Guide - User guide for radio operators
- Contributing Guide - Developer contribution guidelines
- Archive Documentation - Legacy code reference
- HamSCI Workshop 2025 Poster - Project overview and goals
- FRC Proposal - Detailed project proposal
- WSPRDaemon Documentation - PSWS software
- Leaflet Documentation - Map library API
- Turf.js Documentation - Geospatial analysis library
- CQ Zones Map: http://www.cqmaps.com/zones.htm
- ITU Zones Map: http://www.dxmaps.com/ituzone.html
- WSPR Activity: https://wsprnet.org/
- PSK Reporter: https://pskreporter.info/
- Reverse Beacon Network: http://reversebeacon.net/
- Maidenhead Grid Converter: https://www.levinecentral.com/ham/grid_square.php
- Amateur Radio Band Plan: http://www.arrl.org/band-plan
- HamSCI Community: https://hamsci.org/
- TAPR (Tucson Amateur Packet Radio): https://www.tapr.org/
This project is supported by:
- Frankford Radio Club (FRC)
- NSF Grants AGS-2432822, AGS-2432824, and AGS-2432823
- Amateur Radio Digital Communications (ARDC)
- HamSCI Community
- WSPRDaemon and TAPR Communities
This project is licensed under the GNU General Public License v3.0 - see the LICENSE file for details.
Special thanks to:
- Dr. Nathaniel Frissell (W2NAF) - Project advisor
- Ray Sokola (K9RS) - Frankford Radio Club mentor
- Bud Trench (AA3B) - Frankford Radio Club mentor
- Phil Karn (KA9Q) - KA9Q-radio software
- Rob Robinett (AI6VN) - WSPRDaemon software
- University of Scranton W3USR Amateur Radio Club
Project Authors:
- Owen Ruzanski (KD3ALD) - owen.ruzanski@scranton.edu
- Liam Miller (KD3BVX)
- Nathaniel Frissell (W2NAF)
Faculty Advisor: Dr. Nathaniel Frissell (W2NAF) University of Scranton, Department of Physics/Engineering
Dashboard URL: http://dash.kd3ald.com (when operational)
Last updated January 2026 as part of the HamSCI PSWS Dashboard Development project.