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>
This commit is contained in:
root
2026-01-20 11:10:25 -08:00
commit 0c9de30d41
25 changed files with 3498 additions and 0 deletions

24
.gitignore vendored Normal file
View 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
View 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
View 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
View 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
View 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
View File

@@ -0,0 +1,10 @@
{
"receivers": "AUTO",
"receiver_port": 30003,
"location": {
"name": "My Location",
"lat": 0.0,
"lon": 0.0
},
"web_port": 2001
}

BIN
east.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

BIN
happycloud.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

BIN
heavy.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

BIN
helicopter.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

402
index.html Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

BIN
narrowBody.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

BIN
north.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

1897
pixel-view.js Normal file

File diff suppressed because it is too large Load Diff

BIN
plane-right.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

BIN
raincloud.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

BIN
regionalJet.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

378
server.py Executable file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

BIN
south.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

4
start.sh Executable file
View File

@@ -0,0 +1,4 @@
#!/bin/bash
cd "$(dirname "$0")"
echo "Starting Pixel View ADS-B server..."
python3 server.py

BIN
sun.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

BIN
west.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

BIN
wideBody.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB