Initial commit: Pixel-ADSB flight tracker
Retro SNES-style side-view ADS-B aircraft tracker with pixel art sprites, animated celestial bodies, weather visualization, and directional views. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
24
.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
*.so
|
||||||
|
.Python
|
||||||
|
*.egg-info/
|
||||||
|
.eggs/
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# Claude Code
|
||||||
|
.claude/
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Local config (uncomment if you want to ignore local changes)
|
||||||
|
# config.json
|
||||||
103
CLAUDE.md
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
Pixel View is a retro SNES-style side-view flight tracker that displays ADS-B aircraft data with custom pixel art sprites. It's a standalone sub-project within the larger IceNet-ADS-B system.
|
||||||
|
|
||||||
|
## Running the Server
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Start the server (port configured in config.json, default 2001)
|
||||||
|
python3 server.py
|
||||||
|
|
||||||
|
# Or use the startup script
|
||||||
|
./start.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Access at http://localhost:{web_port} (configured in config.json)
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Components
|
||||||
|
|
||||||
|
- **server.py** - Python WebSocket server using aiohttp
|
||||||
|
- Reads configuration from config.json
|
||||||
|
- Auto-scans for ADS-B receivers or connects to configured IPs
|
||||||
|
- Parses SBS/BaseStation format messages from receivers
|
||||||
|
- Broadcasts flight data to connected WebSocket clients every 1 second
|
||||||
|
- Cleans up flights not seen in 60 seconds
|
||||||
|
- Serves static files and receiver location API
|
||||||
|
|
||||||
|
- **config.json** - Configuration file for receiver and location settings
|
||||||
|
- See CONFIG.md for full documentation
|
||||||
|
|
||||||
|
- **pixel-view.js** - JavaScript rendering engine (Canvas-based)
|
||||||
|
- Handles WebSocket connection and flight data updates
|
||||||
|
- Renders 10 FPS retro-style canvas animation
|
||||||
|
- Layer order (bottom to top): sky gradient → clouds → sun → moon → directional background → grid → aircraft → labels
|
||||||
|
- Directional backgrounds include horizon, so low sun/moon is realistically occluded
|
||||||
|
- Aircraft sprites flip horizontally when heading west (track 90°-270°)
|
||||||
|
- View direction rotates between N/E/S/W (arrow keys or A/D), changing the background
|
||||||
|
|
||||||
|
- **index.html** - Main HTML interface with embedded styles
|
||||||
|
|
||||||
|
### Sprite Assets
|
||||||
|
|
||||||
|
All PNG sprites face right (eastward) and are flipped in-canvas for westbound aircraft:
|
||||||
|
- 6 aircraft types: smallProp, regionalJet, narrowBody, wideBody, heavy, helicopter
|
||||||
|
- Directional backgrounds: north.png, south.png, east.png, west.png (1536x1024, shown based on view direction)
|
||||||
|
- Celestial: sun.png, moon_6_phases.png (2x3 sprite sheet)
|
||||||
|
- Weather: happycloud.png (clear), raincloud.png (rain/snow)
|
||||||
|
|
||||||
|
### Aircraft Type Detection Logic
|
||||||
|
|
||||||
|
Priority order in categorization:
|
||||||
|
1. **Helicopter**: altitude < 5000 ft AND speed < 150 knots
|
||||||
|
2. **Heavy (747/A380)**: specific callsigns OR altitude > 42000 ft OR speed > 550 knots
|
||||||
|
3. **Wide Body**: altitude > 40000 ft OR speed > 500 knots
|
||||||
|
4. **Regional Jet**: specific callsigns OR (altitude < 25000 ft AND speed < 350 knots)
|
||||||
|
5. **Small Prop**: N-prefix callsigns OR (altitude < 10000 ft AND speed < 200 knots)
|
||||||
|
6. **Narrow Body**: default for remaining aircraft
|
||||||
|
|
||||||
|
### View Direction Controls
|
||||||
|
|
||||||
|
The viewer can rotate between cardinal directions (N/E/S/W), showing aircraft in a 90° field of view:
|
||||||
|
- **Keyboard**: Left/Right arrow keys or A/D keys
|
||||||
|
- **UI**: Arrow buttons on the interface
|
||||||
|
- Each direction displays a unique background image (north.png, east.png, south.png, west.png)
|
||||||
|
- Sun and moon positions are calculated based on actual azimuth and only appear when in the current field of view
|
||||||
|
|
||||||
|
### External APIs
|
||||||
|
|
||||||
|
- **Open-Meteo**: Weather and sunrise/sunset data (updates every 10 minutes)
|
||||||
|
- **ADS-B Receivers**: SBS/BaseStation protocol on port 30003
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
Python packages required:
|
||||||
|
- aiohttp (web server and WebSocket)
|
||||||
|
- netifaces (network interface scanning)
|
||||||
|
|
||||||
|
No package.json - frontend is vanilla JavaScript with no build step.
|
||||||
|
|
||||||
|
## Key Code Patterns
|
||||||
|
|
||||||
|
- Canvas version parameter on assets (`?v=36`) for cache busting
|
||||||
|
- Aircraft direction: `const isFacingLeft = flight.track > 90 && flight.track < 270`
|
||||||
|
- Moon phases use sprite sheet cropping with 2x3 grid (3 columns, 2 rows)
|
||||||
|
- Flight data stored in global `flights` dict keyed by ICAO hex code
|
||||||
|
|
||||||
|
## Debugging
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check for running server instances
|
||||||
|
ps aux | grep server.py
|
||||||
|
|
||||||
|
# Kill all instances
|
||||||
|
pkill -9 -f server.py
|
||||||
|
|
||||||
|
# Test receiver connectivity (replace with your receiver IP)
|
||||||
|
nc -zv <receiver_ip> 30003
|
||||||
|
```
|
||||||
190
CONFIG.md
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
# Pixel View Configuration Guide
|
||||||
|
|
||||||
|
This guide explains how to configure Pixel View for your ADS-B receiver setup.
|
||||||
|
|
||||||
|
## Configuration File
|
||||||
|
|
||||||
|
Edit `config.json` to customize your installation:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"receivers": "AUTO",
|
||||||
|
"receiver_port": 30003,
|
||||||
|
"location": {
|
||||||
|
"name": "My Location",
|
||||||
|
"lat": 0.0,
|
||||||
|
"lon": 0.0
|
||||||
|
},
|
||||||
|
"web_port": 2001
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Important:** You must set your `location.lat` and `location.lon` to your actual receiver coordinates for weather data and aircraft positioning to work correctly.
|
||||||
|
|
||||||
|
### Configuration Options
|
||||||
|
|
||||||
|
| Option | Type | Description |
|
||||||
|
|--------|------|-------------|
|
||||||
|
| `receivers` | string or array | `"AUTO"` to scan network, or IP address(es) of ADS-B receiver(s) |
|
||||||
|
| `receiver_port` | number | Port for SBS/BaseStation data (default: 30003) |
|
||||||
|
| `location.name` | string | Display name shown on screen (e.g., "Seattle, WA") |
|
||||||
|
| `location.lat` | number | Latitude of your receiver location |
|
||||||
|
| `location.lon` | number | Longitude of your receiver location |
|
||||||
|
| `web_port` | number | Port for the web interface (default: 2001) |
|
||||||
|
|
||||||
|
### Receiver Configuration Examples
|
||||||
|
|
||||||
|
**Auto-scan network (default):**
|
||||||
|
```json
|
||||||
|
"receivers": "AUTO"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Single receiver:**
|
||||||
|
```json
|
||||||
|
"receivers": "192.168.1.100"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Multiple receivers:**
|
||||||
|
```json
|
||||||
|
"receivers": ["192.168.1.100", "192.168.1.101"]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Background Images
|
||||||
|
|
||||||
|
Pixel View uses directional background images to show the horizon view from your receiver location. You should customize these to match your actual surroundings.
|
||||||
|
|
||||||
|
### Image Files
|
||||||
|
|
||||||
|
| File | Direction | Description |
|
||||||
|
|------|-----------|-------------|
|
||||||
|
| `north.png` | North (0°) | View looking north from your location |
|
||||||
|
| `east.png` | East (90°) | View looking east from your location |
|
||||||
|
| `south.png` | South (180°) | View looking south from your location |
|
||||||
|
| `west.png` | West (270°) | View looking west from your location |
|
||||||
|
|
||||||
|
### Image Requirements
|
||||||
|
|
||||||
|
- **Resolution:** 1536 x 1024 pixels (recommended)
|
||||||
|
- **Format:** PNG with transparency support
|
||||||
|
- **Aspect ratio:** 3:2 (width:height)
|
||||||
|
- **Style:** Pixel art style for best visual consistency
|
||||||
|
|
||||||
|
### Image Composition
|
||||||
|
|
||||||
|
Each background image should include:
|
||||||
|
|
||||||
|
1. **Sky area** (top ~60%): Should be transparent or very light to blend with the dynamic sky gradient
|
||||||
|
2. **Horizon line**: Where the sky meets the ground/landscape
|
||||||
|
3. **Ground/landscape** (bottom ~40%): Your local terrain features
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────┐
|
||||||
|
│ │
|
||||||
|
│ Transparent/Sky │ ← Dynamic sky renders here
|
||||||
|
│ (alpha = 0 or light) │
|
||||||
|
│ │
|
||||||
|
├─────────────────────────────┤ ← Horizon line
|
||||||
|
│ │
|
||||||
|
│ Ground/Landscape │ ← Your local scenery
|
||||||
|
│ (mountains, buildings, │
|
||||||
|
│ trees, desert, etc.) │
|
||||||
|
│ │
|
||||||
|
└─────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Creating Custom Backgrounds
|
||||||
|
|
||||||
|
**Option 1: Pixel Art (Recommended)**
|
||||||
|
- Use a pixel art editor (Aseprite, Piskel, GIMP)
|
||||||
|
- Create at 384x256 or 768x512, then scale up 4x or 2x
|
||||||
|
- Keep colors limited for retro aesthetic
|
||||||
|
- Use the existing backgrounds as templates
|
||||||
|
|
||||||
|
**Option 2: Photo-based**
|
||||||
|
- Take photos looking N/E/S/W from your receiver location
|
||||||
|
- Apply a pixel art filter or posterize effect
|
||||||
|
- Reduce to limited color palette
|
||||||
|
- Resize to 1536x1024
|
||||||
|
|
||||||
|
**Option 3: Simplified Silhouettes**
|
||||||
|
- Create simple horizon silhouettes of local landmarks
|
||||||
|
- Mountains, buildings, trees as flat shapes
|
||||||
|
- Works well with limited artistic skills
|
||||||
|
|
||||||
|
### Tips for Good Backgrounds
|
||||||
|
|
||||||
|
1. **Consistency**: Use the same color palette across all 4 directions
|
||||||
|
2. **Horizon height**: Keep the horizon at roughly the same vertical position
|
||||||
|
3. **Landmarks**: Include recognizable local features (mountains, towers, etc.)
|
||||||
|
4. **Weather**: The sky portion should be transparent so the dynamic weather shows through
|
||||||
|
5. **Testing**: View each direction in the app to ensure smooth rotation
|
||||||
|
|
||||||
|
### Example Color Palettes
|
||||||
|
|
||||||
|
**Desert/Southwest:**
|
||||||
|
```
|
||||||
|
Ground tones: #d4a868, #b8884c, #a87840
|
||||||
|
Rock/mountain: #8c7c68, #6c5c4c
|
||||||
|
Vegetation: #54a844, #3c7c30
|
||||||
|
```
|
||||||
|
|
||||||
|
**Forest/Pacific Northwest:**
|
||||||
|
```
|
||||||
|
Ground tones: #3c5c3c, #4c6c4c, #2c4c2c
|
||||||
|
Trees: #2c5c2c, #1c4c1c, #3c6c3c
|
||||||
|
Mountains: #5c6c7c, #7c8c9c, #fcfcfc (snow)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Urban/City:**
|
||||||
|
```
|
||||||
|
Buildings: #4c5c6c, #5c6c7c, #6c7c8c
|
||||||
|
Windows: #fcd444, #fcfc9c
|
||||||
|
Ground: #3c3c3c, #4c4c4c
|
||||||
|
```
|
||||||
|
|
||||||
|
**Coastal:**
|
||||||
|
```
|
||||||
|
Sand: #e4d4a8, #d4c498
|
||||||
|
Water: #5c94fc, #4c84ec
|
||||||
|
Cliffs: #8c7c68, #9c8c78
|
||||||
|
```
|
||||||
|
|
||||||
|
Sky should always be transparent (#00000000) to allow the dynamic sky gradient to show through.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Other Sprite Assets
|
||||||
|
|
||||||
|
These are optional to customize:
|
||||||
|
|
||||||
|
| File | Size | Description |
|
||||||
|
|------|------|-------------|
|
||||||
|
| `sun.png` | 64x64 | Sun sprite |
|
||||||
|
| `moon_6_phases.png` | 192x128 | Moon phases (3x2 grid) |
|
||||||
|
| `happycloud.png` | 96x64 | Clear weather cloud |
|
||||||
|
| `raincloud.png` | 96x64 | Rain/storm cloud |
|
||||||
|
|
||||||
|
### Aircraft Sprites
|
||||||
|
|
||||||
|
| File | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `smallProp.png` | Small propeller aircraft (Cessna) |
|
||||||
|
| `regionalJet.png` | Regional jets (CRJ, ERJ) |
|
||||||
|
| `narrowBody.png` | Narrow body jets (737, A320) |
|
||||||
|
| `wideBody.png` | Wide body jets (777, 787) |
|
||||||
|
| `heavy.png` | Heavy/jumbo jets (747, A380) |
|
||||||
|
| `helicopter.png` | Helicopters |
|
||||||
|
|
||||||
|
All aircraft sprites should face **right (east)** - the code flips them automatically for westbound flights.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick Start Checklist
|
||||||
|
|
||||||
|
1. [ ] Edit `config.json` with your receiver IP (or leave as AUTO)
|
||||||
|
2. [ ] Set your location name, latitude, and longitude
|
||||||
|
3. [ ] Replace `north.png`, `east.png`, `south.png`, `west.png` with your local views
|
||||||
|
4. [ ] Start the server: `python3 server.py`
|
||||||
|
5. [ ] Open browser to `http://your-server-ip:2001`
|
||||||
434
PROJECT_DOCUMENTATION.md
Normal file
@@ -0,0 +1,434 @@
|
|||||||
|
# Pixel ADS-B View - Project Documentation
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
A retro SNES-style side-view flight tracker that displays ADS-B aircraft data in a fun pixel art interface with custom ChatGPT-generated sprites for aircraft, scenery, celestial bodies, and weather.
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
```
|
||||||
|
/root/IceNet-ADS-B/pixel-view/
|
||||||
|
├── index.html - Main HTML interface (v=36)
|
||||||
|
├── pixel-view.js - JavaScript rendering engine
|
||||||
|
├── server.py - Python WebSocket server
|
||||||
|
├── smallProp.png - Small propeller aircraft sprite
|
||||||
|
├── regionalJet.png - Regional jet sprite
|
||||||
|
├── narrowBody.png - Narrow body jet sprite (737/A320)
|
||||||
|
├── wideBody.png - Wide body jet sprite (777/787)
|
||||||
|
├── heavy.png - Jumbo jet sprite (747/A380)
|
||||||
|
├── helicopter.png - Helicopter sprite
|
||||||
|
├── desert.png - Vegas desert landscape background
|
||||||
|
├── base.png - ADS-B receiver station building with antenna
|
||||||
|
├── sun.png - Animated sun sprite
|
||||||
|
├── moon_6_phases.png - Moon sprite sheet (2x3 grid, 6 phases)
|
||||||
|
├── happycloud.png - Sunny weather cloud sprite
|
||||||
|
├── raincloud.png - Rainy/snowy weather cloud sprite
|
||||||
|
└── PROJECT_DOCUMENTATION.md - This file
|
||||||
|
```
|
||||||
|
|
||||||
|
## Current Configuration
|
||||||
|
|
||||||
|
### Display Settings
|
||||||
|
- **Canvas Resolution**: 800x600 pixels
|
||||||
|
- **Container Max Width**: 1400px
|
||||||
|
- **Aircraft Sprite Size**: 110px tall (scaled from original images)
|
||||||
|
- **Moon Size**: 120px tall (200% scale)
|
||||||
|
- **Base Station**: 150px height
|
||||||
|
- **Cloud Height**: 90px (150% scale)
|
||||||
|
- **Sun Size**: 50px tall
|
||||||
|
- **Cache Version**: v=36
|
||||||
|
- **Frame Rate**: 10 FPS (retro feel)
|
||||||
|
|
||||||
|
### Visual Assets
|
||||||
|
All sprites are **COMPLETE** and integrated:
|
||||||
|
|
||||||
|
**Aircraft (6 types):**
|
||||||
|
- ✅ **smallProp.png** (193K) - Right-facing, transparent background
|
||||||
|
- ✅ **regionalJet.png** (185K) - Right-facing, transparent background
|
||||||
|
- ✅ **narrowBody.png** (193K) - Right-facing, transparent background
|
||||||
|
- ✅ **wideBody.png** (264K) - Right-facing, transparent background
|
||||||
|
- ✅ **heavy.png** (361K) - Right-facing, transparent background
|
||||||
|
- ✅ **helicopter.png** (216K) - Right-facing, transparent background
|
||||||
|
|
||||||
|
**Scenery:**
|
||||||
|
- ✅ **desert.png** (371K) - Vegas desert landscape, full-width background
|
||||||
|
- ✅ **base.png** - ADS-B receiver station with antenna tower, transparent background
|
||||||
|
|
||||||
|
**Celestial:**
|
||||||
|
- ✅ **sun.png** - Animated sun sprite
|
||||||
|
- ✅ **moon_6_phases.png** (1.4M) - Sprite sheet with 6 moon phases
|
||||||
|
|
||||||
|
**Weather:**
|
||||||
|
- ✅ **happycloud.png** - Clear weather clouds
|
||||||
|
- ✅ **raincloud.png** - Rain/snow weather clouds
|
||||||
|
|
||||||
|
## Features Implemented
|
||||||
|
|
||||||
|
### 1. Custom Pixel Art Aircraft Rendering
|
||||||
|
- **PNG image-based rendering** with fallback to sprite arrays
|
||||||
|
- Images loaded asynchronously via JavaScript Image objects
|
||||||
|
- Scaled dynamically to 110px height while maintaining aspect ratio
|
||||||
|
- **Horizontal flipping** for westbound aircraft (track > 90° and < 270°)
|
||||||
|
- Visual feedback:
|
||||||
|
- **Approaching**: Full brightness, saturated colors
|
||||||
|
- **Departing**: 85% opacity, desaturated
|
||||||
|
- Located in: `pixel-view.js:1347-1395`
|
||||||
|
|
||||||
|
### 2. Aircraft Type Categorization
|
||||||
|
Six aircraft categories with intelligent detection:
|
||||||
|
|
||||||
|
1. **Helicopter**
|
||||||
|
- Detection: altitude < 5000 ft AND speed < 150 knots
|
||||||
|
- Priority: Checked first before other types
|
||||||
|
|
||||||
|
2. **Heavy** (747, A380)
|
||||||
|
- Callsigns: CPA, UAE, ETH, QTR, SIA, etc.
|
||||||
|
- OR: altitude > 42000 ft OR speed > 550 knots
|
||||||
|
- Features: Upper deck hump visible in sprite
|
||||||
|
|
||||||
|
3. **Wide Body** (777, 787, A350)
|
||||||
|
- High altitude: > 40000 ft OR speed > 500 knots
|
||||||
|
- Larger twin-engine design
|
||||||
|
|
||||||
|
4. **Narrow Body** (737, A320)
|
||||||
|
- Default category for most commercial aircraft
|
||||||
|
- Typical cruise altitudes and speeds
|
||||||
|
|
||||||
|
5. **Regional Jet** (CRJ, ERJ)
|
||||||
|
- Callsigns: SKW, RPA, ASH, PDT, CHQ, ENY
|
||||||
|
- OR: altitude < 25000 ft AND speed < 350 knots
|
||||||
|
|
||||||
|
6. **Small Prop** (Cessna, GA)
|
||||||
|
- Callsigns starting with 'N' or very short
|
||||||
|
- OR: altitude < 10000 ft AND speed < 200 knots
|
||||||
|
|
||||||
|
### 3. Vegas Desert Landscape Background
|
||||||
|
- **Full-width background image** (desert.png)
|
||||||
|
- Cropped Vegas desert scene with mountains
|
||||||
|
- Scales to canvas width while maintaining aspect ratio
|
||||||
|
- Transparent background removed for clean integration
|
||||||
|
- Positioned at bottom of display
|
||||||
|
- Located in: `pixel-view.js:1138-1147`
|
||||||
|
|
||||||
|
### 4. ADS-B Receiver Station
|
||||||
|
- **Base.png sprite** overlaid on desert background
|
||||||
|
- Shows receiver building with antenna tower
|
||||||
|
- 150px height, maintains aspect ratio
|
||||||
|
- Centered horizontally at screen bottom
|
||||||
|
- Transparent background allows desert to show through
|
||||||
|
- Located in: `pixel-view.js:1149-1161`
|
||||||
|
|
||||||
|
### 5. Dynamic Sky Colors (Based on Sun Position)
|
||||||
|
Sky colors change throughout the day based on actual sunrise/sunset times:
|
||||||
|
- **Night**: Dark blue/purple (#1c2c4c → #2c3c5c)
|
||||||
|
- **Dawn**: Orange/pink sunrise (#4c5c8c → #dc8c5c)
|
||||||
|
- **Day**: Bright blue (#6ca4dc → #b4d4ec)
|
||||||
|
- **Dusk**: Orange/purple sunset (#6c5c9c → #dc9c6c)
|
||||||
|
- 30-minute transition periods for smooth changes
|
||||||
|
- Weather adjustments (darker for rain/snow)
|
||||||
|
- Located in: `pixel-view.js:678-795`
|
||||||
|
|
||||||
|
### 6. Accurate Sun Positioning & Sprite
|
||||||
|
**Sun:**
|
||||||
|
- PNG sprite rendering (sun.png)
|
||||||
|
- Astronomical calculations for altitude (0-90°) and azimuth (90-270°)
|
||||||
|
- Moves from east (sunrise) to west (sunset)
|
||||||
|
- 50px height display size
|
||||||
|
- Dims when cloudy, hidden during rain/snow
|
||||||
|
- Located in: `pixel-view.js:940-1027`
|
||||||
|
|
||||||
|
### 7. Realistic Moon Phases
|
||||||
|
**Moon Sprite System:**
|
||||||
|
- **6-phase sprite sheet** (moon_6_phases.png)
|
||||||
|
- 2x3 grid layout:
|
||||||
|
- Row 1: Waxing Crescent, First Quarter, Waxing Gibbous
|
||||||
|
- Row 2: Full Moon, Waning Gibbous, Last Quarter
|
||||||
|
- Waning crescent uses mirrored waxing crescent
|
||||||
|
- **Phase calculation** based on 29.53-day lunar cycle
|
||||||
|
- Scaled to 120px height (200% of original design)
|
||||||
|
- Maintains aspect ratio automatically
|
||||||
|
- Brighter at night (100% alpha), dimmer during day (60% alpha)
|
||||||
|
- Opposite positioning from sun (12-hour offset)
|
||||||
|
- New moon phase not displayed
|
||||||
|
- Located in: `pixel-view.js:876-938`, `pixel-view.js:1029-1138`
|
||||||
|
|
||||||
|
**Moon Phases:**
|
||||||
|
- Phase 0.00-0.05: New Moon (not displayed)
|
||||||
|
- Phase 0.05-0.20: Waxing Crescent
|
||||||
|
- Phase 0.20-0.30: First Quarter
|
||||||
|
- Phase 0.30-0.48: Waxing Gibbous
|
||||||
|
- Phase 0.48-0.52: Full Moon
|
||||||
|
- Phase 0.52-0.70: Waning Gibbous
|
||||||
|
- Phase 0.70-0.80: Last Quarter
|
||||||
|
- Phase 0.80-0.95: Waning Crescent (mirrored)
|
||||||
|
- Phase 0.95-1.00: New Moon (not displayed)
|
||||||
|
|
||||||
|
### 8. Weather Cloud Sprites
|
||||||
|
**Cloud System:**
|
||||||
|
- Automatic cloud type selection based on weather conditions
|
||||||
|
- **happycloud.png**: Clear/normal weather
|
||||||
|
- **raincloud.png**: Rain or snow conditions
|
||||||
|
- 90px height (150% scale), maintains aspect ratio
|
||||||
|
- Animated horizontal scrolling
|
||||||
|
- Randomized vertical positions for natural distribution
|
||||||
|
- Varying speeds for depth effect
|
||||||
|
- 4 clouds for clear weather, 8 clouds for rain/cloudy
|
||||||
|
- NO pixelated weather effects - clouds represent weather visually
|
||||||
|
- Located in: `pixel-view.js:797-850`
|
||||||
|
|
||||||
|
### 9. Sunrise/Sunset Display
|
||||||
|
- Actual sunrise and sunset times from Open-Meteo API
|
||||||
|
- Format: "☀ 6:45 AM / 🌙 5:30 PM"
|
||||||
|
- Updates every 10 minutes
|
||||||
|
- Located in: `index.html:169`
|
||||||
|
|
||||||
|
### 10. Weather Integration
|
||||||
|
- Real-time weather from Open-Meteo API
|
||||||
|
- Conditions:
|
||||||
|
- **Clear**: Sunny clouds, full brightness
|
||||||
|
- **Cloudy**: More clouds, dimmed sun
|
||||||
|
- **Rain**: Rain clouds, darker sky, no sun
|
||||||
|
- **Snow**: Rain clouds, darker sky, no sun
|
||||||
|
- Temperature in Fahrenheit
|
||||||
|
- Updates every 10 minutes
|
||||||
|
- Located in: `pixel-view.js:797-850`
|
||||||
|
|
||||||
|
## Technical Details
|
||||||
|
|
||||||
|
### Image Loading System
|
||||||
|
```javascript
|
||||||
|
// Aircraft images
|
||||||
|
this.aircraftImages = {
|
||||||
|
smallProp: new Image(),
|
||||||
|
regionalJet: new Image(),
|
||||||
|
narrowBody: new Image(),
|
||||||
|
wideBody: new Image(),
|
||||||
|
heavy: new Image(),
|
||||||
|
helicopter: new Image()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Environment images
|
||||||
|
this.desertImage = new Image(); // Vegas desert landscape
|
||||||
|
this.baseImage = new Image(); // Receiver station
|
||||||
|
this.sunImage = new Image(); // Sun sprite
|
||||||
|
this.moonSprite = new Image(); // Moon phases sprite sheet
|
||||||
|
this.happyCloudImage = new Image(); // Clear weather clouds
|
||||||
|
this.rainCloudImage = new Image(); // Rain weather clouds
|
||||||
|
```
|
||||||
|
|
||||||
|
### Rendering Pipeline & Layer Order
|
||||||
|
Layers drawn from bottom to top:
|
||||||
|
|
||||||
|
1. **Sky gradient** - Dynamic colors based on time of day
|
||||||
|
2. **Clouds** - Happy or rain clouds based on weather
|
||||||
|
3. **Sun** - Moves across sky based on actual sun position
|
||||||
|
4. **Moon** - Phase-accurate sprite from sheet
|
||||||
|
5. **Desert background** - Full-width Vegas landscape
|
||||||
|
6. **Grid lines** - Altitude reference (every 10,000 feet)
|
||||||
|
7. **Base station** - ADS-B receiver building with antenna
|
||||||
|
8. **Aircraft** - Scaled, positioned, and oriented correctly
|
||||||
|
9. **Scale indicators** - Altitude labels on left side
|
||||||
|
10. **Compass indicators** - North/South directional arrows
|
||||||
|
|
||||||
|
### Aircraft Direction Logic
|
||||||
|
```javascript
|
||||||
|
// Sprites face right (eastward) by default
|
||||||
|
// Flip horizontally when heading west
|
||||||
|
const isFacingLeft = flight.track > 90 && flight.track < 270;
|
||||||
|
|
||||||
|
// Heading ranges:
|
||||||
|
// 0°-90° (N to E): Face right ➡️
|
||||||
|
// 90°-180° (E to S): Face right ➡️
|
||||||
|
// 180°-270° (S to W): Flip left ⬅️
|
||||||
|
// 270°-360° (W to N): Flip left ⬅️
|
||||||
|
```
|
||||||
|
|
||||||
|
### Moon Sprite Sheet Cropping
|
||||||
|
```javascript
|
||||||
|
// 2x3 grid: 3 columns, 2 rows
|
||||||
|
const spriteWidth = moonSprite.width / 3;
|
||||||
|
const spriteHeight = moonSprite.height / 2;
|
||||||
|
|
||||||
|
// Select sprite based on phase
|
||||||
|
const sx = spritePos.col * spriteWidth; // X position in sheet
|
||||||
|
const sy = spritePos.row * spriteHeight; // Y position in sheet
|
||||||
|
|
||||||
|
// Draw cropped section
|
||||||
|
ctx.drawImage(moonSprite,
|
||||||
|
sx, sy, spriteWidth, spriteHeight, // Source rectangle
|
||||||
|
destX, destY, targetWidth, targetHeight // Destination
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### API Endpoints Used
|
||||||
|
- **Open-Meteo Forecast API**: Weather and sunrise/sunset
|
||||||
|
- Endpoint: `https://api.open-meteo.com/v1/forecast`
|
||||||
|
- Parameters: latitude, longitude, current_weather, daily
|
||||||
|
- Update interval: 10 minutes
|
||||||
|
|
||||||
|
### WebSocket Communication
|
||||||
|
- **Server**: Python 3 with aiohttp
|
||||||
|
- **Port**: 2001
|
||||||
|
- **Protocol**: JSON messages
|
||||||
|
- **Update Rate**: 1 second
|
||||||
|
- **Message Format**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "flights",
|
||||||
|
"flights": [
|
||||||
|
{
|
||||||
|
"icao": "ABC123",
|
||||||
|
"callsign": "UAL123",
|
||||||
|
"lat": 36.1,
|
||||||
|
"lon": -115.2,
|
||||||
|
"altitude": 35000,
|
||||||
|
"speed": 450,
|
||||||
|
"track": 90
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### ADS-B Data Processing
|
||||||
|
- Server scans network for SBS receivers (port 30003)
|
||||||
|
- Common receiver IPs: .38, .100, .101
|
||||||
|
- Parses SBS/BaseStation format messages
|
||||||
|
- Broadcasts to WebSocket clients every 1 second
|
||||||
|
- Cleans up flights not seen in 60 seconds
|
||||||
|
|
||||||
|
### Receiver Location
|
||||||
|
- **Default**: Las Vegas, NV (36.2788°N, 115.2283°W)
|
||||||
|
- **API**: `/api/receiver-location`
|
||||||
|
- **Uses**: Distance calculations, weather data, sun/moon positioning
|
||||||
|
|
||||||
|
## Image Processing Details
|
||||||
|
|
||||||
|
### Original Source
|
||||||
|
- Generated via ChatGPT/DALL-E with custom prompts
|
||||||
|
- Downloaded from SMB share: `\\192.168.0.150\allen\Projects\PixelADSB`
|
||||||
|
- Credentials: allen / *69ChopsOne69*
|
||||||
|
|
||||||
|
### Processing Commands
|
||||||
|
```bash
|
||||||
|
# Download from SMB
|
||||||
|
smbclient //192.168.0.150/allen -U 'allen%*69ChopsOne69*' \
|
||||||
|
-c "cd Projects\\PixelADSB; prompt OFF; mget *.png"
|
||||||
|
|
||||||
|
# Flip aircraft sprites (originally facing left)
|
||||||
|
for img in smallProp.png regionalJet.png narrowBody.png \
|
||||||
|
wideBody.png heavy.png helicopter.png
|
||||||
|
do
|
||||||
|
convert "$img" -flop "$img"
|
||||||
|
done
|
||||||
|
|
||||||
|
# Rename background
|
||||||
|
mv VegasDesertBackground.png desert.png
|
||||||
|
```
|
||||||
|
|
||||||
|
### Image Specifications
|
||||||
|
**Aircraft:**
|
||||||
|
- Original: High-resolution PNG (185K - 361K)
|
||||||
|
- Facing: RIGHT (east) after horizontal flip
|
||||||
|
- Background: Transparent
|
||||||
|
- Display: 110px height, aspect ratio maintained
|
||||||
|
|
||||||
|
**Desert:**
|
||||||
|
- Original: 839K → Cropped: 371K
|
||||||
|
- White background removed (transparent)
|
||||||
|
- Display: Full canvas width, aspect ratio maintained
|
||||||
|
|
||||||
|
**Base Station:**
|
||||||
|
- Transparent background
|
||||||
|
- Display: 150px height, centered
|
||||||
|
|
||||||
|
**Moon:**
|
||||||
|
- Sprite sheet: 1.4M (2x3 grid)
|
||||||
|
- Display: 120px height per phase
|
||||||
|
- Aspect ratio maintained for each phase
|
||||||
|
|
||||||
|
**Sun:**
|
||||||
|
- Display: 50px tall
|
||||||
|
|
||||||
|
**Clouds:**
|
||||||
|
- Display: 90px height
|
||||||
|
- Two variants: happy (clear), rain (bad weather)
|
||||||
|
|
||||||
|
## Server Management
|
||||||
|
|
||||||
|
### Running the Server
|
||||||
|
```bash
|
||||||
|
cd /root/IceNet-ADS-B/pixel-view
|
||||||
|
python3 server.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### Server Access
|
||||||
|
- **Local**: http://192.168.0.254:2001
|
||||||
|
- **WebSocket**: ws://192.168.0.254:2001/ws
|
||||||
|
- **Tailscale Funnel**: Available (check tailscale status)
|
||||||
|
|
||||||
|
### Background Processes
|
||||||
|
Multiple server instances may be running in background.
|
||||||
|
- Check: `ps aux | grep server.py`
|
||||||
|
- Kill all: `pkill -9 -f server.py`
|
||||||
|
|
||||||
|
## Version History
|
||||||
|
- **v=36** (Current): Removed pixelated weather effects
|
||||||
|
- **v=35**: Moon scaled to 200% (120px)
|
||||||
|
- **v=34**: Moon aspect ratio fix
|
||||||
|
- **v=33**: Moon sprite sheet implementation (6 phases)
|
||||||
|
- **v=32**: Aircraft direction logic fixed
|
||||||
|
- **v=31**: Desert background implementation
|
||||||
|
- **v=30**: Desert + base layering
|
||||||
|
- **v=29**: Base station sprite added
|
||||||
|
- **v=24**: Cloud sprites (happy + rain)
|
||||||
|
- **v=20**: Aircraft size 110px
|
||||||
|
- **v=16**: PNG image rendering with custom sprites
|
||||||
|
|
||||||
|
## Key Code Locations
|
||||||
|
|
||||||
|
### Aircraft
|
||||||
|
- `pixel-view.js:85-92` - Aircraft image loading
|
||||||
|
- `pixel-view.js:1347-1349` - Direction logic (flip when westbound)
|
||||||
|
- `pixel-view.js:1374-1393` - PNG rendering with flip
|
||||||
|
|
||||||
|
### Scenery
|
||||||
|
- `pixel-view.js:95-113` - Desert and base image loading
|
||||||
|
- `pixel-view.js:1138-1147` - Desert background rendering (full width)
|
||||||
|
- `pixel-view.js:1149-1161` - Base station rendering (on top)
|
||||||
|
|
||||||
|
### Celestial
|
||||||
|
- `pixel-view.js:115-123` - Sun image loading
|
||||||
|
- `pixel-view.js:145-153` - Moon sprite sheet loading
|
||||||
|
- `pixel-view.js:940-1027` - Sun sprite rendering
|
||||||
|
- `pixel-view.js:1029-1059` - Moon phase calculation
|
||||||
|
- `pixel-view.js:1061-1138` - Moon sprite rendering with cropping
|
||||||
|
|
||||||
|
### Weather
|
||||||
|
- `pixel-view.js:125-143` - Cloud sprite loading
|
||||||
|
- `pixel-view.js:797-850` - Cloud rendering (auto-selects rain vs happy)
|
||||||
|
|
||||||
|
### Sky
|
||||||
|
- `pixel-view.js:678-795` - Dynamic sky colors
|
||||||
|
|
||||||
|
## Removed Features
|
||||||
|
The following pixel-generated features were removed and replaced with sprite images:
|
||||||
|
- ❌ Pixel-generated mountains (replaced by desert.png)
|
||||||
|
- ❌ Pixel-generated cacti (included in desert.png)
|
||||||
|
- ❌ Pixel-generated ADS-B tower (replaced by base.png)
|
||||||
|
- ❌ Pixelated rain/snow effects (weather shown via cloud sprites)
|
||||||
|
|
||||||
|
## Status
|
||||||
|
✅ **COMPLETE** - All features implemented and working
|
||||||
|
- All 6 aircraft sprites integrated
|
||||||
|
- Vegas desert landscape background
|
||||||
|
- ADS-B receiver station sprite
|
||||||
|
- 6-phase moon sprite system
|
||||||
|
- Sun sprite
|
||||||
|
- Weather cloud sprites (happy + rain)
|
||||||
|
- Helicopter detection active
|
||||||
|
- Accurate celestial positioning
|
||||||
|
- Weather-reactive visuals
|
||||||
|
- All sprites properly layered
|
||||||
|
|
||||||
|
---
|
||||||
|
Last Updated: December 30, 2025
|
||||||
|
Project Status: Complete and Operational
|
||||||
|
Current Version: v=36
|
||||||
56
README.md
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
# Pixel-ADSB
|
||||||
|
|
||||||
|
A retro SNES-style side-view flight tracker that displays ADS-B aircraft data with custom pixel art sprites.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Real-time aircraft tracking via ADS-B receivers
|
||||||
|
- Custom pixel art sprites for 6 aircraft types
|
||||||
|
- Animated sun and moon with accurate positions based on location
|
||||||
|
- Weather visualization (clouds, rain/snow)
|
||||||
|
- Directional view (N/E/S/W) with unique backgrounds
|
||||||
|
- Canvas-based 10 FPS retro rendering
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install dependencies
|
||||||
|
pip install aiohttp netifaces
|
||||||
|
|
||||||
|
# Start the server
|
||||||
|
python3 server.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Access at http://localhost:2001 (or configured port in config.json)
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Python 3.8+
|
||||||
|
- ADS-B receiver providing SBS/BaseStation format on port 30003
|
||||||
|
- Modern web browser with Canvas support
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
- [CONFIG.md](CONFIG.md) - Configuration options
|
||||||
|
- [PROJECT_DOCUMENTATION.md](PROJECT_DOCUMENTATION.md) - Technical details
|
||||||
|
- [CLAUDE.md](CLAUDE.md) - Development guidance
|
||||||
|
|
||||||
|
## Aircraft Types
|
||||||
|
|
||||||
|
| Type | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| Small Prop | Light aircraft, N-prefix callsigns |
|
||||||
|
| Regional Jet | Regional carriers |
|
||||||
|
| Narrow Body | 737/A320 class |
|
||||||
|
| Wide Body | 777/787 class |
|
||||||
|
| Heavy | 747/A380 class |
|
||||||
|
| Helicopter | Low altitude, slow speed |
|
||||||
|
|
||||||
|
## Controls
|
||||||
|
|
||||||
|
- **Arrow Keys / A/D**: Rotate view direction
|
||||||
|
- View cycles through North, East, South, West
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
10
config.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"receivers": "AUTO",
|
||||||
|
"receiver_port": 30003,
|
||||||
|
"location": {
|
||||||
|
"name": "My Location",
|
||||||
|
"lat": 0.0,
|
||||||
|
"lon": 0.0
|
||||||
|
},
|
||||||
|
"web_port": 2001
|
||||||
|
}
|
||||||
BIN
happycloud.png
Normal file
|
After Width: | Height: | Size: 60 KiB |
BIN
helicopter.png
Normal file
|
After Width: | Height: | Size: 33 KiB |
402
index.html
Normal file
@@ -0,0 +1,402 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||||
|
<meta name="mobile-web-app-capable" content="yes">
|
||||||
|
<title>Pixel ADS-B - Retro Flight View</title>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
position: fixed;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background: linear-gradient(180deg, #2c3e50 0%, #34495e 100%);
|
||||||
|
color: #fcfcfc;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
padding: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left);
|
||||||
|
}
|
||||||
|
|
||||||
|
#header {
|
||||||
|
text-align: center;
|
||||||
|
padding: 8px;
|
||||||
|
font-size: clamp(18px, 5vw, 28px);
|
||||||
|
color: #fcd444;
|
||||||
|
text-shadow: 2px 2px 0px #000, -1px -1px 0px #000, 1px -1px 0px #000, -1px 1px 0px #000;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
font-weight: bold;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#main-content {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 1600px;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
#canvas-container {
|
||||||
|
position: relative;
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#pixel-canvas {
|
||||||
|
image-rendering: pixelated;
|
||||||
|
image-rendering: crisp-edges;
|
||||||
|
border: 4px solid #34495e;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.4),
|
||||||
|
inset 0 0 0 2px #5c94fc;
|
||||||
|
background: #000;
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Aircraft Sidebar */
|
||||||
|
#aircraft-sidebar {
|
||||||
|
width: 200px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
background: rgba(52, 73, 94, 0.9);
|
||||||
|
border: 3px solid #5c94fc;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 8px;
|
||||||
|
overflow-y: auto;
|
||||||
|
max-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sidebar-header {
|
||||||
|
font-family: 'Press Start 2P', monospace;
|
||||||
|
font-size: 10px;
|
||||||
|
color: #fcd444;
|
||||||
|
text-align: center;
|
||||||
|
padding: 8px 4px;
|
||||||
|
border-bottom: 2px solid #5c94fc;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-aircraft {
|
||||||
|
padding: 6px 8px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
background: rgba(0, 0, 0, 0.4);
|
||||||
|
border-radius: 4px;
|
||||||
|
border-left: 3px solid #54fc54;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-aircraft:hover {
|
||||||
|
background: rgba(92, 148, 252, 0.3);
|
||||||
|
border-left-color: #fcd444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-aircraft.selected {
|
||||||
|
background: rgba(252, 212, 68, 0.3);
|
||||||
|
border-left-color: #fcd444;
|
||||||
|
border-left-width: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-aircraft.selected .sidebar-callsign {
|
||||||
|
color: #fcd444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-callsign {
|
||||||
|
display: block;
|
||||||
|
font-family: 'Press Start 2P', monospace;
|
||||||
|
font-size: 9px;
|
||||||
|
color: #fcfcfc;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-info {
|
||||||
|
display: block;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 11px;
|
||||||
|
color: #b4d4ec;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-empty {
|
||||||
|
font-family: 'Press Start 2P', monospace;
|
||||||
|
font-size: 8px;
|
||||||
|
color: #888;
|
||||||
|
text-align: center;
|
||||||
|
padding: 20px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#stats {
|
||||||
|
text-align: center;
|
||||||
|
padding: 8px;
|
||||||
|
font-size: clamp(11px, 2.5vw, 15px);
|
||||||
|
color: #fcfcfc;
|
||||||
|
text-shadow: 1px 1px 0px #000;
|
||||||
|
font-weight: bold;
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#stats span {
|
||||||
|
background: rgba(52, 73, 94, 0.7);
|
||||||
|
padding: 5px 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 2px solid #5c94fc;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
#weather-info {
|
||||||
|
text-align: center;
|
||||||
|
padding: 6px;
|
||||||
|
font-size: clamp(10px, 2.2vw, 14px);
|
||||||
|
color: #fcfcfc;
|
||||||
|
text-shadow: 1px 1px 0px #000;
|
||||||
|
font-weight: bold;
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#weather-info span {
|
||||||
|
background: rgba(52, 73, 94, 0.7);
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 2px solid #5c94fc;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
#datetime-display {
|
||||||
|
color: #b4d4ec;
|
||||||
|
}
|
||||||
|
|
||||||
|
#weather-display {
|
||||||
|
color: #fcd444;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sun-times {
|
||||||
|
color: #fc9c54;
|
||||||
|
}
|
||||||
|
|
||||||
|
#connection-status {
|
||||||
|
color: #54fc54;
|
||||||
|
}
|
||||||
|
|
||||||
|
#aircraft-count {
|
||||||
|
color: #fcd444;
|
||||||
|
}
|
||||||
|
|
||||||
|
#range-display {
|
||||||
|
color: #fc9c54;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blink {
|
||||||
|
animation: blink 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes blink {
|
||||||
|
0%, 49% { opacity: 1; }
|
||||||
|
50%, 100% { opacity: 0.3; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* View direction controls */
|
||||||
|
.view-arrow {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
width: 50px;
|
||||||
|
height: 80px;
|
||||||
|
background: rgba(52, 73, 94, 0.8);
|
||||||
|
border: 3px solid #5c94fc;
|
||||||
|
border-radius: 8px;
|
||||||
|
color: #fcd444;
|
||||||
|
font-size: 28px;
|
||||||
|
font-family: 'Press Start 2P', monospace;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
z-index: 10;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.view-arrow:hover {
|
||||||
|
background: rgba(92, 148, 252, 0.6);
|
||||||
|
transform: translateY(-50%) scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.view-arrow:active {
|
||||||
|
background: rgba(92, 148, 252, 0.9);
|
||||||
|
transform: translateY(-50%) scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
#view-left {
|
||||||
|
left: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#view-right {
|
||||||
|
right: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#compass-direction {
|
||||||
|
color: #54fc54;
|
||||||
|
font-family: 'Press Start 2P', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile responsive - move sidebar below viewer */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
#main-content {
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 5px;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#aircraft-sidebar {
|
||||||
|
width: 100%;
|
||||||
|
max-height: 150px;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
display: flex;
|
||||||
|
align-content: flex-start;
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sidebar-header {
|
||||||
|
width: 100%;
|
||||||
|
padding: 5px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
font-size: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 4px;
|
||||||
|
width: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
|
max-height: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-aircraft {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
padding: 4px 8px;
|
||||||
|
margin-bottom: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-callsign {
|
||||||
|
font-size: 8px;
|
||||||
|
display: inline;
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-info {
|
||||||
|
font-size: 9px;
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-empty {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#stats {
|
||||||
|
padding: 4px;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#stats span {
|
||||||
|
padding: 3px 6px;
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#weather-info {
|
||||||
|
padding: 4px;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#weather-info span {
|
||||||
|
padding: 3px 6px;
|
||||||
|
font-size: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.view-arrow {
|
||||||
|
width: 40px;
|
||||||
|
height: 60px;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#view-left {
|
||||||
|
left: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#view-right {
|
||||||
|
right: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="header">
|
||||||
|
★ SKY WATCH ★
|
||||||
|
</div>
|
||||||
|
<div id="main-content">
|
||||||
|
<div id="canvas-container">
|
||||||
|
<button id="view-left" class="view-arrow">◄</button>
|
||||||
|
<canvas id="pixel-canvas" width="800" height="600"></canvas>
|
||||||
|
<button id="view-right" class="view-arrow">►</button>
|
||||||
|
</div>
|
||||||
|
<div id="aircraft-sidebar">
|
||||||
|
<div id="sidebar-header">AIRCRAFT IN VIEW</div>
|
||||||
|
<div class="sidebar-list"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="stats">
|
||||||
|
<span id="connection-status" class="blink">CONNECTING...</span>
|
||||||
|
<span id="compass-direction">VIEWING: NORTH</span>
|
||||||
|
<span id="aircraft-count">AIRCRAFT: 0</span>
|
||||||
|
<span id="range-display">RANGE: 0 NM</span>
|
||||||
|
</div>
|
||||||
|
<div id="weather-info">
|
||||||
|
<span id="datetime-display">Loading...</span>
|
||||||
|
<span id="weather-display">--°F</span>
|
||||||
|
<span id="sun-times">☀ --:-- / 🌙 --:--</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="pixel-view.js?v=47"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
BIN
moon_6_phases.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
BIN
narrowBody.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
1897
pixel-view.js
Normal file
BIN
plane-right.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
raincloud.png
Normal file
|
After Width: | Height: | Size: 2.2 MiB |
BIN
regionalJet.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
378
server.py
Executable file
@@ -0,0 +1,378 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Pixel ADS-B Server - Connects directly to ADS-B receivers"""
|
||||||
|
import asyncio
|
||||||
|
import socket
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
import ipaddress
|
||||||
|
from pathlib import Path
|
||||||
|
from dataclasses import dataclass, asdict
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Dict, Set, List
|
||||||
|
from aiohttp import web
|
||||||
|
import netifaces
|
||||||
|
|
||||||
|
WEB_DIR = Path(__file__).parent
|
||||||
|
CONFIG_FILE = WEB_DIR / "config.json"
|
||||||
|
|
||||||
|
# Default configuration
|
||||||
|
config = {
|
||||||
|
"receivers": "AUTO",
|
||||||
|
"receiver_port": 30003,
|
||||||
|
"location": {
|
||||||
|
"name": "My Location",
|
||||||
|
"lat": 0.0,
|
||||||
|
"lon": 0.0
|
||||||
|
},
|
||||||
|
"web_port": 2001
|
||||||
|
}
|
||||||
|
|
||||||
|
def load_config():
|
||||||
|
"""Load configuration from config.json"""
|
||||||
|
global config
|
||||||
|
if CONFIG_FILE.exists():
|
||||||
|
try:
|
||||||
|
with open(CONFIG_FILE, 'r') as f:
|
||||||
|
loaded = json.load(f)
|
||||||
|
config.update(loaded)
|
||||||
|
print(f"Loaded config from {CONFIG_FILE}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error loading config: {e}, using defaults")
|
||||||
|
else:
|
||||||
|
print(f"No config file found at {CONFIG_FILE}, using defaults")
|
||||||
|
|
||||||
|
print(f" Receivers: {config['receivers']}")
|
||||||
|
print(f" Receiver port: {config['receiver_port']}")
|
||||||
|
print(f" Location: {config['location']['name']} ({config['location']['lat']}, {config['location']['lon']})")
|
||||||
|
print(f" Web port: {config['web_port']}")
|
||||||
|
|
||||||
|
# Flight data storage
|
||||||
|
flights: Dict[str, dict] = {}
|
||||||
|
connected_clients: Set = set()
|
||||||
|
receivers: List[str] = []
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class SBSMessage:
|
||||||
|
msg_type: str
|
||||||
|
icao: str
|
||||||
|
callsign: str = ""
|
||||||
|
altitude: int = 0
|
||||||
|
speed: float = 0
|
||||||
|
track: float = 0
|
||||||
|
lat: float = 0
|
||||||
|
lon: float = 0
|
||||||
|
vrate: int = 0
|
||||||
|
squawk: str = ""
|
||||||
|
ground: bool = False
|
||||||
|
|
||||||
|
def parse_sbs_message(line: str):
|
||||||
|
"""Parse SBS/BaseStation format message"""
|
||||||
|
parts = line.strip().split(',')
|
||||||
|
if len(parts) < 10:
|
||||||
|
return None
|
||||||
|
|
||||||
|
msg_type = parts[0]
|
||||||
|
if msg_type not in ['MSG']:
|
||||||
|
return None
|
||||||
|
|
||||||
|
transmission_type = parts[1]
|
||||||
|
icao = parts[4].strip()
|
||||||
|
if not icao:
|
||||||
|
return None
|
||||||
|
|
||||||
|
msg = SBSMessage(msg_type=msg_type, icao=icao)
|
||||||
|
|
||||||
|
# Callsign (field 10)
|
||||||
|
if len(parts) > 10 and parts[10].strip():
|
||||||
|
msg.callsign = parts[10].strip()
|
||||||
|
|
||||||
|
# Altitude (field 11)
|
||||||
|
if len(parts) > 11 and parts[11].strip():
|
||||||
|
try:
|
||||||
|
msg.altitude = int(parts[11])
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Ground speed (field 12)
|
||||||
|
if len(parts) > 12 and parts[12].strip():
|
||||||
|
try:
|
||||||
|
msg.speed = float(parts[12])
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Track (field 13)
|
||||||
|
if len(parts) > 13 and parts[13].strip():
|
||||||
|
try:
|
||||||
|
msg.track = float(parts[13])
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Latitude (field 14)
|
||||||
|
if len(parts) > 14 and parts[14].strip():
|
||||||
|
try:
|
||||||
|
msg.lat = float(parts[14])
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Longitude (field 15)
|
||||||
|
if len(parts) > 15 and parts[15].strip():
|
||||||
|
try:
|
||||||
|
msg.lon = float(parts[15])
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Vertical rate (field 16)
|
||||||
|
if len(parts) > 16 and parts[16].strip():
|
||||||
|
try:
|
||||||
|
msg.vrate = int(parts[16])
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Squawk (field 17)
|
||||||
|
if len(parts) > 17 and parts[17].strip():
|
||||||
|
msg.squawk = parts[17].strip()
|
||||||
|
|
||||||
|
# Ground flag (field 21)
|
||||||
|
if len(parts) > 21 and parts[21].strip() == '-1':
|
||||||
|
msg.ground = True
|
||||||
|
|
||||||
|
return msg
|
||||||
|
|
||||||
|
async def connect_to_receiver(host: str, port: int = 30003):
|
||||||
|
"""Connect to an SBS receiver and process messages"""
|
||||||
|
print(f"Connecting to receiver at {host}:{port}...")
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
reader, writer = await asyncio.open_connection(host, port)
|
||||||
|
print(f"Connected to {host}:{port}")
|
||||||
|
|
||||||
|
while True:
|
||||||
|
line = await reader.readline()
|
||||||
|
if not line:
|
||||||
|
break
|
||||||
|
|
||||||
|
line = line.decode('utf-8', errors='ignore')
|
||||||
|
msg = parse_sbs_message(line)
|
||||||
|
if msg and msg.icao:
|
||||||
|
# Update flight data
|
||||||
|
if msg.icao not in flights:
|
||||||
|
flights[msg.icao] = {
|
||||||
|
'icao': msg.icao,
|
||||||
|
'callsign': '',
|
||||||
|
'altitude': 0,
|
||||||
|
'speed': 0,
|
||||||
|
'track': 0,
|
||||||
|
'lat': 0,
|
||||||
|
'lon': 0,
|
||||||
|
'vrate': 0,
|
||||||
|
'squawk': '',
|
||||||
|
'ground': False,
|
||||||
|
'last_seen': time.time()
|
||||||
|
}
|
||||||
|
|
||||||
|
flight = flights[msg.icao]
|
||||||
|
if msg.callsign:
|
||||||
|
flight['callsign'] = msg.callsign
|
||||||
|
if msg.altitude:
|
||||||
|
flight['altitude'] = msg.altitude
|
||||||
|
if msg.speed:
|
||||||
|
flight['speed'] = msg.speed
|
||||||
|
if msg.track:
|
||||||
|
flight['track'] = msg.track
|
||||||
|
if msg.lat:
|
||||||
|
flight['lat'] = msg.lat
|
||||||
|
if msg.lon:
|
||||||
|
flight['lon'] = msg.lon
|
||||||
|
if msg.vrate:
|
||||||
|
flight['vrate'] = msg.vrate
|
||||||
|
if msg.squawk:
|
||||||
|
flight['squawk'] = msg.squawk
|
||||||
|
flight['ground'] = msg.ground
|
||||||
|
flight['last_seen'] = time.time()
|
||||||
|
|
||||||
|
writer.close()
|
||||||
|
await writer.wait_closed()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Receiver {host}:{port} error: {e}")
|
||||||
|
await asyncio.sleep(5) # Wait before reconnecting
|
||||||
|
|
||||||
|
async def scan_for_receivers():
|
||||||
|
"""Scan local network for ADS-B receivers on port 30003"""
|
||||||
|
print("Scanning for ADS-B receivers on port 30003...")
|
||||||
|
|
||||||
|
# Get local network
|
||||||
|
for iface in netifaces.interfaces():
|
||||||
|
addrs = netifaces.ifaddresses(iface)
|
||||||
|
if netifaces.AF_INET in addrs:
|
||||||
|
for addr in addrs[netifaces.AF_INET]:
|
||||||
|
ip = addr.get('addr')
|
||||||
|
netmask = addr.get('netmask')
|
||||||
|
if ip and netmask and not ip.startswith('127.'):
|
||||||
|
# Calculate network from IP and netmask
|
||||||
|
try:
|
||||||
|
network = ipaddress.IPv4Network(f"{ip}/{netmask}", strict=False)
|
||||||
|
print(f"Scanning {network} ({network.num_addresses} hosts)...")
|
||||||
|
|
||||||
|
# Get all host IPs in the network
|
||||||
|
host_ips = [str(host) for host in network.hosts()]
|
||||||
|
|
||||||
|
# Scan all IPs in parallel
|
||||||
|
async def check_ip(test_ip):
|
||||||
|
if await test_port(test_ip, 30003):
|
||||||
|
return test_ip
|
||||||
|
return None
|
||||||
|
|
||||||
|
results = await asyncio.gather(*[check_ip(host) for host in host_ips])
|
||||||
|
for result in results:
|
||||||
|
if result:
|
||||||
|
print(f"Found receiver at {result}:30003")
|
||||||
|
receivers.append(result)
|
||||||
|
except ValueError as e:
|
||||||
|
print(f"Invalid network {ip}/{netmask}: {e}")
|
||||||
|
|
||||||
|
async def test_port(ip: str, port: int, timeout: float = 0.5):
|
||||||
|
"""Test if a port is open"""
|
||||||
|
try:
|
||||||
|
conn = asyncio.open_connection(ip, port)
|
||||||
|
reader, writer = await asyncio.wait_for(conn, timeout=timeout)
|
||||||
|
writer.close()
|
||||||
|
await writer.wait_closed()
|
||||||
|
return True
|
||||||
|
except:
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def cleanup_old_flights():
|
||||||
|
"""Remove flights not seen in 60 seconds"""
|
||||||
|
while True:
|
||||||
|
await asyncio.sleep(10)
|
||||||
|
now = time.time()
|
||||||
|
to_remove = [icao for icao, flight in flights.items()
|
||||||
|
if now - flight['last_seen'] > 60]
|
||||||
|
for icao in to_remove:
|
||||||
|
del flights[icao]
|
||||||
|
|
||||||
|
async def broadcast_flights():
|
||||||
|
"""Broadcast flight data to all connected WebSocket clients"""
|
||||||
|
while True:
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
if connected_clients:
|
||||||
|
# Get flights with position data
|
||||||
|
flight_list = [f for f in flights.values() if f['lat'] and f['lon']]
|
||||||
|
print(f"Broadcasting {len(flight_list)} flights to {len(connected_clients)} clients")
|
||||||
|
print(f"Total flights in memory: {len(flights)}")
|
||||||
|
message = json.dumps({
|
||||||
|
'type': 'flights',
|
||||||
|
'flights': flight_list,
|
||||||
|
'count': len(flight_list)
|
||||||
|
})
|
||||||
|
|
||||||
|
# Send to all connected clients
|
||||||
|
dead_clients = set()
|
||||||
|
for client in connected_clients:
|
||||||
|
try:
|
||||||
|
await client.send_str(message)
|
||||||
|
except:
|
||||||
|
dead_clients.add(client)
|
||||||
|
|
||||||
|
# Remove disconnected clients
|
||||||
|
connected_clients.difference_update(dead_clients)
|
||||||
|
|
||||||
|
async def websocket_handler(request):
|
||||||
|
"""Handle WebSocket connections from browser"""
|
||||||
|
ws = web.WebSocketResponse()
|
||||||
|
await ws.prepare(request)
|
||||||
|
|
||||||
|
connected_clients.add(ws)
|
||||||
|
print(f"WebSocket client connected ({len(connected_clients)} total)")
|
||||||
|
|
||||||
|
try:
|
||||||
|
async for msg in ws:
|
||||||
|
pass # We don't expect messages from client
|
||||||
|
finally:
|
||||||
|
connected_clients.discard(ws)
|
||||||
|
print(f"WebSocket client disconnected ({len(connected_clients)} remaining)")
|
||||||
|
|
||||||
|
return ws
|
||||||
|
|
||||||
|
async def handle_receiver_location(request):
|
||||||
|
"""Return receiver location from config"""
|
||||||
|
return web.json_response({
|
||||||
|
"lat": config["location"]["lat"],
|
||||||
|
"lon": config["location"]["lon"],
|
||||||
|
"name": config["location"]["name"]
|
||||||
|
})
|
||||||
|
|
||||||
|
async def handle_http(request):
|
||||||
|
"""Serve static files"""
|
||||||
|
path = request.path
|
||||||
|
if path == '/':
|
||||||
|
path = '/index.html'
|
||||||
|
|
||||||
|
file_path = WEB_DIR / path.lstrip('/')
|
||||||
|
if file_path.exists() and file_path.is_file():
|
||||||
|
return web.FileResponse(file_path)
|
||||||
|
return web.Response(status=404, text="Not Found")
|
||||||
|
|
||||||
|
async def start_http_server():
|
||||||
|
"""Start HTTP server with WebSocket support"""
|
||||||
|
port = config["web_port"]
|
||||||
|
app = web.Application()
|
||||||
|
app.router.add_get('/ws', websocket_handler)
|
||||||
|
app.router.add_get('/api/receiver-location', handle_receiver_location)
|
||||||
|
app.router.add_get('/{tail:.*}', handle_http)
|
||||||
|
|
||||||
|
runner = web.AppRunner(app)
|
||||||
|
await runner.setup()
|
||||||
|
site = web.TCPSite(runner, '0.0.0.0', port)
|
||||||
|
await site.start()
|
||||||
|
print(f"HTTP server with WebSocket running on http://0.0.0.0:{port}")
|
||||||
|
print(f"WebSocket available at ws://0.0.0.0:{port}/ws")
|
||||||
|
print(f"Receiver location API at http://0.0.0.0:{port}/api/receiver-location")
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
"""Main entry point"""
|
||||||
|
global receivers
|
||||||
|
|
||||||
|
print("Pixel ADS-B Server Starting...")
|
||||||
|
|
||||||
|
# Load configuration
|
||||||
|
load_config()
|
||||||
|
|
||||||
|
print(f"Access at http://0.0.0.0:{config['web_port']}")
|
||||||
|
|
||||||
|
# Get receivers based on config
|
||||||
|
receiver_config = config["receivers"]
|
||||||
|
receiver_port = config["receiver_port"]
|
||||||
|
|
||||||
|
if receiver_config == "AUTO":
|
||||||
|
# Auto-scan for receivers
|
||||||
|
await scan_for_receivers()
|
||||||
|
elif isinstance(receiver_config, list):
|
||||||
|
# Use specified receiver IPs
|
||||||
|
receivers = receiver_config
|
||||||
|
print(f"Using configured receivers: {receivers}")
|
||||||
|
elif isinstance(receiver_config, str):
|
||||||
|
# Single receiver IP specified
|
||||||
|
receivers = [receiver_config]
|
||||||
|
print(f"Using configured receiver: {receivers[0]}")
|
||||||
|
|
||||||
|
if not receivers:
|
||||||
|
print("WARNING: No receivers found or configured!")
|
||||||
|
else:
|
||||||
|
print(f"Using {len(receivers)} receiver(s)")
|
||||||
|
|
||||||
|
# Start all tasks
|
||||||
|
tasks = [
|
||||||
|
start_http_server(),
|
||||||
|
cleanup_old_flights(),
|
||||||
|
broadcast_flights(),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Connect to all receivers
|
||||||
|
for receiver in receivers:
|
||||||
|
tasks.append(connect_to_receiver(receiver, receiver_port))
|
||||||
|
|
||||||
|
await asyncio.gather(*tasks)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
BIN
smallProp.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
4
start.sh
Executable file
@@ -0,0 +1,4 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
echo "Starting Pixel View ADS-B server..."
|
||||||
|
python3 server.py
|
||||||
BIN
wideBody.png
Normal file
|
After Width: | Height: | Size: 31 KiB |